diff --git a/include/dolphin/gx/GXAurora.h b/include/dolphin/gx/GXAurora.h index e3fb746..f743515 100644 --- a/include/dolphin/gx/GXAurora.h +++ b/include/dolphin/gx/GXAurora.h @@ -83,19 +83,19 @@ void GXPopDebugGroup(); void GXInsertDebugMarker(const char* label); typedef enum _AuroraViewportPolicy { - AURORA_VIEWPORT_FIT = 0, // Scale logical viewport to fit - AURORA_VIEWPORT_STRETCH = 1, // Stretch logical viewport to fit (widescreen enabled) - AURORA_VIEWPORT_NATIVE = 2, // Use native framebuffer resolution + AURORA_VIEWPORT_FIT = 0, // Preserve logical aspect in the content framebuffer + AURORA_VIEWPORT_STRETCH = 1, // Match content framebuffer aspect to the native surface + AURORA_VIEWPORT_NATIVE = 2, // Use active framebuffer pixels directly } AuroraViewportPolicy; /** - * Configures how GXSetViewport/GXSetScissor parameters are applied to the actual render viewport. + * Configures content framebuffer sizing and how GXSetViewport/GXSetScissor parameters are applied to rendering. * When AURORA_VIEWPORT_NATIVE is used, GXSetTexCopySrc/GXSetTexCopyDst will use native framebuffer resolution. */ void AuroraSetViewportPolicy(AuroraViewportPolicy policy); /** - * Retrieves the current render size based on the framebuffer size and viewport policy. + * Retrieves the current content framebuffer size. */ void AuroraGetRenderSize(u32* width, u32* height); diff --git a/lib/aurora.cpp b/lib/aurora.cpp index 35b760d..720941d 100644 --- a/lib/aurora.cpp +++ b/lib/aurora.cpp @@ -69,36 +69,6 @@ constexpr std::array PreferredBackendOrder{}; bool g_initialFrame = false; -#ifdef AURORA_ENABLE_GX -gfx::Viewport calculate_present_viewport(uint32_t surface_width, uint32_t surface_height, uint32_t content_width, - uint32_t content_height) { - if (surface_width == 0 || surface_height == 0 || content_width == 0 || content_height == 0) { - return {}; - } - - uint32_t viewport_width = surface_width; - uint32_t viewport_height = std::min( - surface_height, std::max(1u, static_cast(std::lround(static_cast(viewport_width) * - static_cast(content_height) / - static_cast(content_width))))); - if (viewport_height == surface_height) { - viewport_width = std::min( - surface_width, std::max(1u, static_cast(std::lround(static_cast(viewport_height) * - static_cast(content_width) / - static_cast(content_height))))); - } - - return { - .left = static_cast((surface_width - viewport_width) / 2), - .top = static_cast((surface_height - viewport_height) / 2), - .width = static_cast(viewport_width), - .height = static_cast(viewport_height), - .znear = 0.f, - .zfar = 1.f, - }; -} -#endif - AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexcept { g_config = config; Log.info("Aurora initializing"); @@ -270,19 +240,18 @@ void end_frame() noexcept { auto encoder = g_device.CreateCommandEncoder(&encoderDescriptor); gfx::end_frame(encoder); gfx::render(encoder); - const auto viewport = calculate_present_viewport(webgpu::g_graphicsConfig.surfaceConfiguration.width, - webgpu::g_graphicsConfig.surfaceConfiguration.height, - webgpu::g_frameBuffer.size.width, webgpu::g_frameBuffer.size.height); + const auto& presentSource = webgpu::present_source(); + auto viewport = webgpu::calculate_present_viewport(webgpu::g_graphicsConfig.surfaceConfiguration.width, + webgpu::g_graphicsConfig.surfaceConfiguration.height, + presentSource.size.width, presentSource.size.height); + wgpu::BindGroup presentBindGroup = webgpu::g_CopyBindGroup; #if AURORA_ENABLE_RMLUI if (rmlui::is_initialized()) { - rmlui::render(encoder, g_currentView, - { - .width = webgpu::g_graphicsConfig.surfaceConfiguration.width, - .height = webgpu::g_graphicsConfig.surfaceConfiguration.height, - .depthOrArrayLayers = 1, - }, - viewport); - } else + const auto rmlOutput = rmlui::render(encoder, viewport); + if (rmlOutput.texture != nullptr) { + presentBindGroup = rmlOutput.copyBindGroup; + } + } #endif { const std::array attachments{ @@ -300,7 +269,7 @@ void end_frame() noexcept { const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); // Copy EFB -> XFB (swapchain) pass.SetPipeline(webgpu::g_CopyPipeline); - pass.SetBindGroup(0, webgpu::g_CopyBindGroup, 0, nullptr); + pass.SetBindGroup(0, presentBindGroup, 0, nullptr); pass.SetViewport(viewport.left, viewport.top, viewport.width, viewport.height, viewport.znear, viewport.zfar); pass.Draw(3); @@ -320,7 +289,8 @@ void end_frame() noexcept { .colorAttachments = attachments.data(), }; const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); - pass.SetViewport(viewport.left, viewport.top, viewport.width, viewport.height, viewport.znear, viewport.zfar); + pass.SetViewport(0.f, 0.f, static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.width), + static_cast(webgpu::g_graphicsConfig.surfaceConfiguration.height), 0.f, 1.f); imgui::render(pass); pass.End(); } diff --git a/lib/dolphin/gx/GXAurora.cpp b/lib/dolphin/gx/GXAurora.cpp index e71b481..a82fbef 100644 --- a/lib/dolphin/gx/GXAurora.cpp +++ b/lib/dolphin/gx/GXAurora.cpp @@ -1,7 +1,5 @@ #include "dolphin/gx/GXAurora.h" -#include -#include #include #include "__gx.h" @@ -10,7 +8,6 @@ #include "../../gfx/common.hpp" #include "../../gx/fifo.hpp" -#include "../vi/vi_internal.hpp" static void GXWriteString(const char* label) { auto length = strlen(label); @@ -36,33 +33,18 @@ void GXInsertDebugMarker(const char* label) { GXWriteString(label); } -void AuroraSetViewportPolicy(AuroraViewportPolicy policy) { g_gxState.viewportPolicy = policy; } +void AuroraSetViewportPolicy(AuroraViewportPolicy policy) { + g_gxState.viewportPolicy = policy; + aurora::window::set_frame_buffer_aspect_fit(policy == AURORA_VIEWPORT_FIT); +} void AuroraGetRenderSize(u32* width, u32* height) { const auto windowSize = aurora::window::get_window_size(); - u32 renderWidth = windowSize.fb_width; - u32 renderHeight = windowSize.fb_height; - - if (g_gxState.viewportPolicy == AURORA_VIEWPORT_FIT) { - const auto efbSize = aurora::vi::configured_fb_size(); - if (efbSize.x != 0 && efbSize.y != 0 && renderWidth != 0 && renderHeight != 0) { - const double targetAspect = static_cast(renderWidth) / static_cast(renderHeight); - const double contentAspect = static_cast(efbSize.x) / static_cast(efbSize.y); - if (targetAspect > contentAspect) { - renderWidth = - std::max(1u, static_cast(std::lround(static_cast(renderHeight) * contentAspect))); - } else { - renderHeight = - std::max(1u, static_cast(std::lround(static_cast(renderWidth) / contentAspect))); - } - } - } - if (width != nullptr) { - *width = renderWidth; + *width = windowSize.fb_width; } if (height != nullptr) { - *height = renderHeight; + *height = windowSize.fb_height; } } diff --git a/lib/dolphin/vi/vi.cpp b/lib/dolphin/vi/vi.cpp index 7530fa0..4c8dddc 100644 --- a/lib/dolphin/vi/vi.cpp +++ b/lib/dolphin/vi/vi.cpp @@ -9,20 +9,28 @@ namespace aurora::vi { std::optional g_renderMode; -void configure(const GXRenderModeObj* rm) noexcept { - if (rm == nullptr) { - g_renderMode.reset(); - return; - } - g_renderMode = *rm; -} - -Vec2 configured_fb_size() noexcept { +Vec2 render_mode_size() noexcept { if (!g_renderMode) { return {640, 480}; } return {g_renderMode->fbWidth, g_renderMode->efbHeight}; } + +void configure(const GXRenderModeObj* rm) noexcept { + const auto oldSize = render_mode_size(); + if (rm == nullptr) { + g_renderMode.reset(); + } else { + g_renderMode = *rm; + } + if (render_mode_size() != oldSize) { + window::request_frame_buffer_resize(); + } +} + +Vec2 configured_fb_size() noexcept { + return render_mode_size(); +} } // namespace aurora::vi extern "C" { diff --git a/lib/gfx/common.hpp b/lib/gfx/common.hpp index 96584be..9a5b69a 100644 --- a/lib/gfx/common.hpp +++ b/lib/gfx/common.hpp @@ -1,6 +1,7 @@ #pragma once #include "../internal.hpp" +#include "../webgpu/gpu.hpp" #include #include @@ -208,20 +209,7 @@ struct ClipRect { bool operator!=(const ClipRect& rhs) const { return !(*this == rhs); } }; -struct Viewport { - float left; - float top; - float width; - float height; - float znear; - float zfar; - - bool operator==(const Viewport& rhs) const { - return left == rhs.left && top == rhs.top && width == rhs.width && height == rhs.height && znear == rhs.znear && - zfar == rhs.zfar; - } - bool operator!=(const Viewport& rhs) const { return !(*this == rhs); } -}; +using webgpu::Viewport; struct TextureRef; using TextureHandle = std::shared_ptr; diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index 52cc0b4..d8ee7f1 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -246,31 +246,6 @@ Vec2 logical_fb_size() noexcept { return gfx::is_offscreen() ? gfx::get_render_target_size() : vi::configured_fb_size(); } -std::tuple calculate_inner_box(const int targetWidth, const int targetHeight, - const int logicalFbWidth, const int logicalFbHeight) noexcept { - if (g_gxState.viewportPolicy == AURORA_VIEWPORT_STRETCH) { - return { - 0.0f, - 0.0f, - static_cast(targetWidth), - static_cast(targetHeight), - }; - } else { - const auto scale = std::min(static_cast(targetWidth) / static_cast(logicalFbWidth), - static_cast(targetHeight) / static_cast(logicalFbHeight)); - const auto offsetX = - std::trunc((static_cast(targetWidth) - static_cast(logicalFbWidth) * scale) * 0.5f); - const auto offsetY = - std::trunc((static_cast(targetHeight) - static_cast(logicalFbHeight) * scale) * 0.5f); - return { - offsetX, - offsetY, - static_cast(targetWidth) - 2.0f * offsetX, - static_cast(targetHeight) - 2.0f * offsetY, - }; - } -} - gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept { if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { return logicalViewport; @@ -282,14 +257,13 @@ gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcep return logicalViewport; } - const auto [offsetX, offsetY, innerWidth, innerHeight] = - calculate_inner_box(targetWidth, targetHeight, logicalFbWidth, logicalFbHeight); - + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); return { - .left = offsetX + (logicalViewport.left / static_cast(logicalFbWidth)) * innerWidth, - .top = offsetY + (logicalViewport.top / static_cast(logicalFbHeight)) * innerHeight, - .width = (logicalViewport.width / static_cast(logicalFbWidth)) * innerWidth, - .height = (logicalViewport.height / static_cast(logicalFbHeight)) * innerHeight, + .left = logicalViewport.left * scaleX, + .top = logicalViewport.top * scaleY, + .width = logicalViewport.width * scaleX, + .height = logicalViewport.height * scaleY, .znear = logicalViewport.znear, .zfar = logicalViewport.zfar, }; @@ -306,13 +280,13 @@ gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept return logicalScissor; } - const auto [offsetX, offsetY, innerWidth, innerHeight] = - calculate_inner_box(targetWidth, targetHeight, logicalFbWidth, logicalFbHeight); + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); - const float left = offsetX + (static_cast(logicalScissor.x) / static_cast(logicalFbWidth)) * innerWidth; - const float top = offsetY + (static_cast(logicalScissor.y) / static_cast(logicalFbHeight)) * innerHeight; - const float right = offsetX + (static_cast(logicalScissor.x + logicalScissor.width) / static_cast(logicalFbWidth)) * innerWidth; - const float bottom = offsetY + (static_cast(logicalScissor.y + logicalScissor.height) / static_cast(logicalFbHeight)) * innerHeight; + const float left = static_cast(logicalScissor.x) * scaleX; + const float top = static_cast(logicalScissor.y) * scaleY; + const float right = static_cast(logicalScissor.x + logicalScissor.width) * scaleX; + const float bottom = static_cast(logicalScissor.y + logicalScissor.height) * scaleY; const auto mappedLeft = std::clamp(static_cast(std::floor(left)), 0, static_cast(targetWidth)); const auto mappedTop = std::clamp(static_cast(std::floor(top)), 0, static_cast(targetHeight)); diff --git a/lib/rmlui.cpp b/lib/rmlui.cpp index 4f8b162..63099ab 100644 --- a/lib/rmlui.cpp +++ b/lib/rmlui.cpp @@ -13,21 +13,292 @@ #include "webgpu/gpu.hpp" namespace aurora::rmlui { +Rml::Context* g_context = nullptr; +FileInterface_SDL* g_fileInterface = nullptr; + namespace { Module Log("aurora::rmlui"); +constexpr size_t MaxTrackedTouches = 16; + +struct TrackedTouch { + SDL_FingerID id = 0; + Rml::Vector2f position; + bool active = false; +}; + +uint32_t s_pressedMouseButtons = 0; +std::array s_trackedTouches{}; +webgpu::TextureWithSampler s_renderTarget; +wgpu::BindGroup s_renderTargetCopyBindGroup; WebGPURenderInterface* get_render_interface() noexcept { return static_cast(Backend::GetRenderInterface()); // NOLINT(*-pro-type-static-cast-downcast) } + +Rml::Vector2i dimensions_from_viewport(const gfx::Viewport& viewport) noexcept { + return { + std::max(1, static_cast(std::lround(viewport.width))), + std::max(1, static_cast(std::lround(viewport.height))), + }; +} + +Rml::Vector2i presentation_dimensions_from_window_size(const AuroraWindowSize& size) noexcept { + const auto viewport = + webgpu::calculate_present_viewport(size.native_fb_width, size.native_fb_height, size.fb_width, size.fb_height); + return dimensions_from_viewport(viewport); +} + +float density_ratio_for_window(const AuroraWindowSize& size) noexcept { + if (size.width == 0 || size.height == 0 || size.native_fb_width == 0 || size.native_fb_height == 0) { + return 1.f; + } + + return std::min(static_cast(size.native_fb_width) / static_cast(size.width), + static_cast(size.native_fb_height) / static_cast(size.height)); +} + +void sync_context_metrics(Rml::Vector2i dimensions) noexcept { + if (g_context == nullptr || dimensions.x <= 0 || dimensions.y <= 0) { + return; + } + if (g_context->GetDimensions() != dimensions) { + g_context->SetDimensions(dimensions); + } + const float ratio = density_ratio_for_window(window::get_window_size()); + if (g_context->GetDensityIndependentPixelRatio() != ratio) { + g_context->SetDensityIndependentPixelRatio(ratio); + } +} + +void ensure_render_target(Rml::Vector2i dimensions) noexcept { + if (dimensions.x <= 0 || dimensions.y <= 0) { + return; + } + const auto width = static_cast(dimensions.x); + const auto height = static_cast(dimensions.y); + if (s_renderTarget.view && s_renderTarget.size.width == width && s_renderTarget.size.height == height) { + return; + } + s_renderTarget = webgpu::create_render_texture(width, height, false); + s_renderTargetCopyBindGroup = webgpu::create_copy_bind_group(s_renderTarget); +} + +void copy_game_to_render_target(const wgpu::CommandEncoder& encoder) noexcept { + if (!s_renderTarget.view) { + return; + } + const std::array attachments{ + wgpu::RenderPassColorAttachment{ + .view = s_renderTarget.view, + .loadOp = wgpu::LoadOp::Clear, + .storeOp = wgpu::StoreOp::Store, + }, + }; + const wgpu::RenderPassDescriptor renderPassDescriptor{ + .label = "RmlUi game frame copy pass", + .colorAttachmentCount = attachments.size(), + .colorAttachments = attachments.data(), + }; + const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); + pass.SetPipeline(webgpu::g_CopyPipeline); + pass.SetBindGroup(0, webgpu::g_CopyBindGroup, 0, nullptr); + pass.SetViewport(0.f, 0.f, static_cast(s_renderTarget.size.width), + static_cast(s_renderTarget.size.height), 0.f, 1.f); + pass.Draw(3); + pass.End(); +} + +struct MappedPoint { + Rml::Vector2f position; + bool inside = false; +}; + +MappedPoint map_native_point_to_content(float nativeX, float nativeY) noexcept { + if (g_context == nullptr) { + return {}; + } + + const auto size = window::get_window_size(); + const Rml::Vector2i contentSize = g_context->GetDimensions(); + if (size.native_fb_width == 0 || size.native_fb_height == 0 || contentSize.x <= 0 || contentSize.y <= 0) { + return {}; + } + + const auto viewport = + webgpu::calculate_present_viewport(size.native_fb_width, size.native_fb_height, + static_cast(contentSize.x), static_cast(contentSize.y)); + const float right = viewport.left + viewport.width; + const float bottom = viewport.top + viewport.height; + if (viewport.width <= 0.f || viewport.height <= 0.f || nativeX < viewport.left || nativeY < viewport.top || + nativeX >= right || nativeY >= bottom) { + return {}; + } + + return { + .position = + { + (nativeX - viewport.left) * static_cast(contentSize.x) / viewport.width, + (nativeY - viewport.top) * static_cast(contentSize.y) / viewport.height, + }, + .inside = true, + }; +} + +MappedPoint map_window_point_to_content(float windowX, float windowY) noexcept { + const auto size = window::get_window_size(); + if (size.width == 0 || size.height == 0) { + return {}; + } + return map_native_point_to_content( + windowX * static_cast(size.native_fb_width) / static_cast(size.width), + windowY * static_cast(size.native_fb_height) / static_cast(size.height)); +} + +MappedPoint map_touch_point_to_content(const SDL_TouchFingerEvent& event) noexcept { + const auto size = window::get_window_size(); + return map_native_point_to_content(event.x * static_cast(size.native_fb_width), + event.y * static_cast(size.native_fb_height)); +} + +int rounded_content_coord(float value) noexcept { return static_cast(std::floor(value)); } + +bool mouse_button_tracked(uint8_t button) noexcept { + return button < 32 && (s_pressedMouseButtons & (1u << button)) != 0; +} + +void set_mouse_button_tracked(uint8_t button, bool tracked) noexcept { + if (button >= 32) { + return; + } + const uint32_t mask = 1u << button; + if (tracked) { + s_pressedMouseButtons |= mask; + } else { + s_pressedMouseButtons &= ~mask; + } +} + +TrackedTouch* find_tracked_touch(SDL_FingerID id) noexcept { + for (auto& touch : s_trackedTouches) { + if (touch.active && touch.id == id) { + return &touch; + } + } + return nullptr; +} + +TrackedTouch* find_free_touch() noexcept { + for (auto& touch : s_trackedTouches) { + if (!touch.active) { + return &touch; + } + } + return nullptr; +} + +Rml::TouchList touch_list(SDL_FingerID id, Rml::Vector2f position) { + return {Rml::Touch{static_cast(id), position}}; +} + +void handle_mouse_motion(const SDL_MouseMotionEvent& motion) noexcept { + const MappedPoint mapped = map_window_point_to_content(motion.x, motion.y); + if (!mapped.inside) { + g_context->ProcessMouseLeave(); + return; + } + g_context->ProcessMouseMove(rounded_content_coord(mapped.position.x), rounded_content_coord(mapped.position.y), + RmlSDL::GetKeyModifierState()); +} + +void handle_mouse_button_down(const SDL_MouseButtonEvent& button) noexcept { + const MappedPoint mapped = map_window_point_to_content(button.x, button.y); + if (!mapped.inside) { + g_context->ProcessMouseLeave(); + return; + } + g_context->ProcessMouseMove(rounded_content_coord(mapped.position.x), rounded_content_coord(mapped.position.y), + RmlSDL::GetKeyModifierState()); + g_context->ProcessMouseButtonDown(RmlSDL::ConvertMouseButton(button.button), RmlSDL::GetKeyModifierState()); + set_mouse_button_tracked(button.button, true); + SDL_CaptureMouse(true); +} + +void handle_mouse_button_up(const SDL_MouseButtonEvent& button) noexcept { + if (!mouse_button_tracked(button.button)) { + return; + } + const auto mapped = map_window_point_to_content(button.x, button.y); + if (mapped.inside) { + g_context->ProcessMouseMove(rounded_content_coord(mapped.position.x), rounded_content_coord(mapped.position.y), + RmlSDL::GetKeyModifierState()); + } else { + g_context->ProcessMouseLeave(); + } + g_context->ProcessMouseButtonUp(RmlSDL::ConvertMouseButton(button.button), RmlSDL::GetKeyModifierState()); + set_mouse_button_tracked(button.button, false); + if (s_pressedMouseButtons == 0) { + SDL_CaptureMouse(false); + } +} + +void handle_mouse_wheel(const SDL_MouseWheelEvent& wheel) noexcept { + float mouseX = 0.f; + float mouseY = 0.f; + SDL_GetMouseState(&mouseX, &mouseY); + if (!map_window_point_to_content(mouseX, mouseY).inside) { + g_context->ProcessMouseLeave(); + return; + } + g_context->ProcessMouseWheel(Rml::Vector2f{wheel.x, -wheel.y}, RmlSDL::GetKeyModifierState()); +} + +void handle_touch_down(const SDL_TouchFingerEvent& finger) noexcept { + if (find_tracked_touch(finger.fingerID) != nullptr) { + return; + } + const auto mapped = map_touch_point_to_content(finger); + if (!mapped.inside) { + return; + } + auto* tracked = find_free_touch(); + if (tracked == nullptr) { + return; + } + *tracked = { + .id = finger.fingerID, + .position = mapped.position, + .active = true, + }; + g_context->ProcessTouchStart(touch_list(finger.fingerID, mapped.position), RmlSDL::GetKeyModifierState()); +} + +void handle_touch_motion(const SDL_TouchFingerEvent& finger) noexcept { + auto* tracked = find_tracked_touch(finger.fingerID); + if (tracked == nullptr) { + return; + } + const auto mapped = map_touch_point_to_content(finger); + if (!mapped.inside) { + return; + } + tracked->position = mapped.position; + g_context->ProcessTouchMove(touch_list(finger.fingerID, mapped.position), RmlSDL::GetKeyModifierState()); +} + +void handle_touch_up(const SDL_TouchFingerEvent& finger) noexcept { + auto* tracked = find_tracked_touch(finger.fingerID); + if (tracked == nullptr) { + return; + } + const auto position = tracked->position; + *tracked = {}; + g_context->ProcessTouchEnd(touch_list(finger.fingerID, position), RmlSDL::GetKeyModifierState()); +} } // namespace -Rml::Context* g_context = nullptr; -FileInterface_SDL* g_fileInterface = nullptr; - void initialize(const AuroraWindowSize& size) noexcept { - int width = static_cast(size.native_fb_width); - int height = static_cast(size.native_fb_height); - if (!Backend::Initialize("Aurora RmlUi Backend", width, height, false)) { + const Rml::Vector2i dim = presentation_dimensions_from_window_size(size); + if (!Backend::Initialize("Aurora RmlUi Backend", dim.x, dim.y, false)) { Log.error("Failed to initialize RmlUI Backend!"); return; } @@ -39,16 +310,17 @@ void initialize(const AuroraWindowSize& size) noexcept { Rml::SetRenderInterface(renderInterface); Rml::SetFileInterface(g_fileInterface); - renderInterface->SetWindowSize({width, height}); + renderInterface->SetWindowSize(dim); renderInterface->SetRenderTargetFormat(webgpu::g_graphicsConfig.surfaceConfiguration.format); renderInterface->CreateDeviceObjects(); Rml::Initialise(); - g_context = Rml::CreateContext("main", Rml::Vector2i(width, height)); + g_context = Rml::CreateContext("main", dim); - if (!g_context) { + if (g_context) { + sync_context_metrics(dim); + } else { Log.error("Failed to initialize RmlUI Context!"); - Rml::Shutdown(); Backend::Shutdown(); } @@ -70,32 +342,73 @@ void handle_event(SDL_Event& event) noexcept { return; } - // RmlSDL::InputEventHandler only passes through -wheel.y, but we want horizontal scrolling too - if (event.type == SDL_EVENT_MOUSE_WHEEL) { - g_context->ProcessMouseWheel(Rml::Vector2f{event.wheel.x, -event.wheel.y}, RmlSDL::GetKeyModifierState()); + switch (event.type) { + case SDL_EVENT_MOUSE_MOTION: + handle_mouse_motion(event.motion); return; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + handle_mouse_button_down(event.button); + return; + case SDL_EVENT_MOUSE_BUTTON_UP: + handle_mouse_button_up(event.button); + return; + case SDL_EVENT_MOUSE_WHEEL: + handle_mouse_wheel(event.wheel); + return; + case SDL_EVENT_FINGER_DOWN: + handle_touch_down(event.tfinger); + return; + case SDL_EVENT_FINGER_MOTION: + handle_touch_motion(event.tfinger); + return; + case SDL_EVENT_FINGER_UP: + case SDL_EVENT_FINGER_CANCELED: + handle_touch_up(event.tfinger); + return; + case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: + sync_context_metrics(presentation_dimensions_from_window_size(window::get_window_size())); + return; + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + g_context->ProcessMouseLeave(); + return; + case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: + sync_context_metrics(presentation_dimensions_from_window_size(window::get_window_size())); + return; + default: + break; } RmlSDL::InputEventHandler(g_context, window::get_sdl_window(), event); } -void render(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& outputView, const wgpu::Extent3D& size, - const gfx::Viewport& viewport) noexcept { +RenderOutput render(const wgpu::CommandEncoder& encoder, const webgpu::Viewport& presentViewport) noexcept { if (g_context == nullptr) { - return; + return {}; } + const Rml::Vector2i dim = dimensions_from_viewport(presentViewport); + ensure_render_target(dim); + if (!s_renderTarget.view) { + return {}; + } + + copy_game_to_render_target(encoder); + sync_context_metrics(dim); g_context->Update(); auto* renderInterface = get_render_interface(); renderInterface->SetWindowSize(g_context->GetDimensions()); - renderInterface->BeginFrame(encoder, outputView, size, viewport); + renderInterface->BeginFrame(encoder, s_renderTarget); Backend::BeginFrame(); g_context->Render(); Backend::PresentFrame(); renderInterface->EndFrame(); + return { + .texture = &s_renderTarget, + .copyBindGroup = s_renderTargetCopyBindGroup, + }; } void shutdown() noexcept { @@ -106,5 +419,7 @@ void shutdown() noexcept { Rml::Shutdown(); Backend::Shutdown(); g_context = nullptr; + s_renderTarget = {}; + s_renderTargetCopyBindGroup = {}; } } // namespace aurora::rmlui diff --git a/lib/rmlui.hpp b/lib/rmlui.hpp index cf264a6..48d38d3 100644 --- a/lib/rmlui.hpp +++ b/lib/rmlui.hpp @@ -1,16 +1,20 @@ #pragma once #include "aurora/aurora.h" -#include "gfx/common.hpp" +#include "webgpu/gpu.hpp" #include #include #include namespace aurora::rmlui { +struct RenderOutput { + const webgpu::TextureWithSampler* texture = nullptr; + wgpu::BindGroup copyBindGroup; +}; + void initialize(const AuroraWindowSize& size) noexcept; void handle_event(SDL_Event& event) noexcept; -void render(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& outputView, const wgpu::Extent3D& size, - const gfx::Viewport& viewport) noexcept; +RenderOutput render(const wgpu::CommandEncoder& encoder, const webgpu::Viewport& presentViewport) noexcept; void shutdown() noexcept; } // namespace aurora::rmlui diff --git a/lib/rmlui/SystemInterface_Aurora.cpp b/lib/rmlui/SystemInterface_Aurora.cpp index 23e51d3..3ed6322 100644 --- a/lib/rmlui/SystemInterface_Aurora.cpp +++ b/lib/rmlui/SystemInterface_Aurora.cpp @@ -6,6 +6,7 @@ #include "../logging.hpp" #include "../window.hpp" +#include "../webgpu/gpu.hpp" namespace aurora::rmlui { namespace { @@ -51,16 +52,36 @@ void SystemInterface_Aurora::ActivateKeyboard(Rml::Vector2f caret_position, floa } const AuroraWindowSize size = window::get_window_size(); - if (size.native_fb_width == 0 || size.native_fb_height == 0 || size.width == 0 || size.height == 0) { + if (size.native_fb_width == 0 || size.native_fb_height == 0 || size.width == 0 || size.height == 0 || + size.fb_width == 0 || size.fb_height == 0) { return; } - const float scaleX = static_cast(size.width) / static_cast(size.native_fb_width); - const float scaleY = static_cast(size.height) / static_cast(size.native_fb_height); - const float inputLineHeight = std::max(line_height * scaleY, 1.0f); + Rml::Vector2i contentSize{static_cast(size.fb_width), static_cast(size.fb_height)}; + if (const Rml::Context* context = get_context()) { + contentSize = context->GetDimensions(); + } + if (contentSize.x <= 0 || contentSize.y <= 0) { + return; + } + + const auto viewport = webgpu::calculate_present_viewport(size.native_fb_width, size.native_fb_height, + static_cast(contentSize.x), + static_cast(contentSize.y)); + if (viewport.width <= 0.f || viewport.height <= 0.f) { + return; + } + + const float nativeToWindowX = static_cast(size.width) / static_cast(size.native_fb_width); + const float nativeToWindowY = static_cast(size.height) / static_cast(size.native_fb_height); + const float contentToNativeX = viewport.width / static_cast(contentSize.x); + const float contentToNativeY = viewport.height / static_cast(contentSize.y); + const float inputLineHeight = std::max(line_height * contentToNativeY * nativeToWindowY, 1.0f); + const float nativeCaretX = viewport.left + caret_position.x * contentToNativeX; + const float nativeCaretY = viewport.top + caret_position.y * contentToNativeY; const SDL_Rect rect = { - .x = static_cast(std::lround(caret_position.x * scaleX)), - .y = static_cast(std::lround((caret_position.y * scaleY) + inputLineHeight)), + .x = static_cast(std::lround(nativeCaretX * nativeToWindowX)), + .y = static_cast(std::lround(nativeCaretY * nativeToWindowY + inputLineHeight)), .w = 1, .h = static_cast(std::lround(inputLineHeight)), }; diff --git a/lib/rmlui/WebGPURenderInterface.cpp b/lib/rmlui/WebGPURenderInterface.cpp index 1444d0e..198d4e0 100644 --- a/lib/rmlui/WebGPURenderInterface.cpp +++ b/lib/rmlui/WebGPURenderInterface.cpp @@ -721,11 +721,14 @@ void WebGPURenderInterface::EnsureRenderTarget(RenderTarget& target, const char* }; target.texture = webgpu::g_device.CreateTexture(&textureDesc); target.view = target.texture.CreateView(nullptr); + target.bindGroup = CreateImageBindGroup(target.view); +} +wgpu::BindGroup WebGPURenderInterface::CreateImageBindGroup(const wgpu::TextureView& view) const { const std::array bindGroupEntries{ wgpu::BindGroupEntry{ .binding = 0, - .textureView = target.view, + .textureView = view, }, }; const wgpu::BindGroupDescriptor bindGroupDesc{ @@ -733,7 +736,7 @@ void WebGPURenderInterface::EnsureRenderTarget(RenderTarget& target, const char* .entryCount = bindGroupEntries.size(), .entries = bindGroupEntries.data(), }; - target.bindGroup = webgpu::g_device.CreateBindGroup(&bindGroupDesc); + return webgpu::g_device.CreateBindGroup(&bindGroupDesc); } void WebGPURenderInterface::EnsureFrameTargets(const wgpu::Extent3D& size) { @@ -741,7 +744,6 @@ void WebGPURenderInterface::EnsureFrameTargets(const wgpu::Extent3D& size) { m_layers.resize(1); } - EnsureRenderTarget(m_layers[0], "RmlUi Base Layer", size); EnsureRenderTarget(m_postprocessTargets[0], "RmlUi Postprocess A", size); EnsureRenderTarget(m_postprocessTargets[1], "RmlUi Postprocess B", size); EnsureRenderTarget(m_postprocessTargets[2], "RmlUi Postprocess C", size); @@ -1041,38 +1043,30 @@ void WebGPURenderInterface::RenderFilters(Rml::Span(target.size.width), + .height = static_cast(target.size.height), + .znear = 0.f, + .zfar = 1.f, + }; m_nextLayer = 1; m_layerStack = {0}; m_activeLayer = 0; NewFrame(); - EnsureFrameTargets(size); - - const std::array copyAttachments{ - wgpu::RenderPassColorAttachment{ - .view = m_layers[0].view, - .loadOp = wgpu::LoadOp::Clear, - .storeOp = wgpu::StoreOp::Store, - .clearValue = {0.f, 0.f, 0.f, 0.f}, - }, + EnsureFrameTargets(target.size); + m_layers[0] = { + .texture = target.texture, + .view = target.view, + .bindGroup = CreateImageBindGroup(target.view), + .size = target.size, }; - const wgpu::RenderPassDescriptor copyPassDesc{ - .label = "RmlUi game frame copy pass", - .colorAttachmentCount = copyAttachments.size(), - .colorAttachments = copyAttachments.data(), - }; - m_pass = m_encoder.BeginRenderPass(©PassDesc); - ApplyViewport(); - m_pass.SetPipeline(webgpu::g_CopyPipeline); - m_pass.SetBindGroup(0, webgpu::g_CopyBindGroup, 0, nullptr); - m_pass.Draw(3); - EndActivePass(); BeginRenderTargetPass(m_layers[0].view, wgpu::LoadOp::Load, "RmlUi base layer pass", true); } @@ -1080,35 +1074,11 @@ void WebGPURenderInterface::BeginFrame(const wgpu::CommandEncoder& encoder, cons void WebGPURenderInterface::EndFrame() { EndActivePass(); - const wgpu::RenderPassDepthStencilAttachment depthStencilAttachment{ - .view = GetClipMaskStencilView(m_frameSize), - .stencilLoadOp = wgpu::LoadOp::Load, - .stencilStoreOp = wgpu::StoreOp::Store, - }; - const std::array attachments{ - wgpu::RenderPassColorAttachment{ - .view = m_outputView, - .loadOp = wgpu::LoadOp::Clear, - .storeOp = wgpu::StoreOp::Store, - .clearValue = {0.f, 0.f, 0.f, 0.f}, - }, - }; - const wgpu::RenderPassDescriptor renderPassDesc{ - .label = "RmlUi output copy pass", - .colorAttachmentCount = attachments.size(), - .colorAttachments = attachments.data(), - .depthStencilAttachment = &depthStencilAttachment, - }; - m_pass = m_encoder.BeginRenderPass(&renderPassDesc); - m_pass.SetViewport(0.f, 0.f, static_cast(m_frameSize.width), static_cast(m_frameSize.height), 0.f, 1.f); - m_pass.SetScissorRect(0, 0, m_frameSize.width, m_frameSize.height); - m_pass.SetStencilReference(0); - DrawFullscreenTexture(m_layers[0].bindGroup, m_blitPipelines[static_cast(BlitPipelineType::Replace)]); - EndActivePass(); - m_layerStack.clear(); + if (!m_layers.empty()) { + m_layers[0] = {}; + } m_encoder = nullptr; - m_outputView = nullptr; } Rml::LayerHandle WebGPURenderInterface::PushLayer() { diff --git a/lib/rmlui/WebGPURenderInterface.hpp b/lib/rmlui/WebGPURenderInterface.hpp index 1040175..0b598dc 100644 --- a/lib/rmlui/WebGPURenderInterface.hpp +++ b/lib/rmlui/WebGPURenderInterface.hpp @@ -7,6 +7,10 @@ #include "../gfx/clear.hpp" #include "RmlUi/Core/RenderInterface.h" +namespace aurora::webgpu { +struct TextureWithSampler; +} // namespace aurora::webgpu + namespace aurora::rmlui { inline constexpr uint32_t MaxHighQualityBlurRadius = 64; @@ -92,7 +96,6 @@ class WebGPURenderInterface : public Rml::RenderInterface { }; wgpu::CommandEncoder m_encoder; - wgpu::TextureView m_outputView; wgpu::RenderPassEncoder m_pass; std::array(PipelineType::Count)> m_pipelines; @@ -156,6 +159,7 @@ class WebGPURenderInterface : public Rml::RenderInterface { void EnsureRenderTarget(RenderTarget& target, const char* label, const wgpu::Extent3D& size); void EnsureFrameTargets(const wgpu::Extent3D& size); + wgpu::BindGroup CreateImageBindGroup(const wgpu::TextureView& view) const; Rml::Rectanglei GetActiveScissorRegion() const; TexCoordLimits GetPostprocessTexCoordLimits() const; TexCoordLimits GetPostprocessTexCoordLimits(Rml::Rectanglei region) const; @@ -205,8 +209,7 @@ public: Rml::TextureHandle texture) override; void ReleaseShader(Rml::CompiledShaderHandle shader) override; - void BeginFrame(const wgpu::CommandEncoder& encoder, const wgpu::TextureView& outputView, const wgpu::Extent3D& size, - const gfx::Viewport& viewport); + void BeginFrame(const wgpu::CommandEncoder& encoder, const webgpu::TextureWithSampler& target); void EndFrame(); void SetWindowSize(const Rml::Vector2i& window_size) { m_windowSize = window_size; } void SetRenderTargetFormat(wgpu::TextureFormat render_target_format) { m_renderTargetFormat = render_target_format; } diff --git a/lib/webgpu/gpu.cpp b/lib/webgpu/gpu.cpp index 4d463df..df4f8b1 100644 --- a/lib/webgpu/gpu.cpp +++ b/lib/webgpu/gpu.cpp @@ -1,7 +1,7 @@ #include "gpu.hpp" #include -#include +#include #include #include #include @@ -9,7 +9,6 @@ #include #include #include -#include #include #include "../gfx/common.hpp" @@ -127,6 +126,38 @@ TextureWithSampler create_render_texture(uint32_t width, uint32_t height, bool m }; } +const TextureWithSampler& present_source() noexcept { + return g_graphicsConfig.msaaSamples > 1 ? g_frameBufferResolved : g_frameBuffer; +} + +Viewport calculate_present_viewport(uint32_t surface_width, uint32_t surface_height, uint32_t content_width, + uint32_t content_height) noexcept { + if (surface_width == 0 || surface_height == 0 || content_width == 0 || content_height == 0) { + return {}; + } + + uint32_t viewport_width = surface_width; + uint32_t viewport_height = std::min( + surface_height, std::max(1u, static_cast(std::lround(static_cast(viewport_width) * + static_cast(content_height) / + static_cast(content_width))))); + if (viewport_height == surface_height) { + viewport_width = std::min( + surface_width, std::max(1u, static_cast(std::lround(static_cast(viewport_height) * + static_cast(content_width) / + static_cast(content_height))))); + } + + return { + .left = static_cast((surface_width - viewport_width) / 2), + .top = static_cast((surface_height - viewport_height) / 2), + .width = static_cast(viewport_width), + .height = static_cast(viewport_height), + .znear = 0.f, + .zfar = 1.f, + }; +} + static TextureWithSampler create_depth_texture(uint32_t width, uint32_t height) { const wgpu::Extent3D size{ .width = width, @@ -277,15 +308,15 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { g_CopyPipeline = g_device.CreateRenderPipeline(&pipelineDescriptor); } -void create_copy_bind_group() { +wgpu::BindGroup create_copy_bind_group(const TextureWithSampler& source) { const std::array bindGroupEntries{ wgpu::BindGroupEntry{ .binding = 0, - .sampler = g_graphicsConfig.msaaSamples > 1 ? g_frameBufferResolved.sampler : g_frameBuffer.sampler, + .sampler = source.sampler, }, wgpu::BindGroupEntry{ .binding = 1, - .textureView = g_graphicsConfig.msaaSamples > 1 ? g_frameBufferResolved.view : g_frameBuffer.view, + .textureView = source.view, }, }; const wgpu::BindGroupDescriptor bindGroupDescriptor{ @@ -293,7 +324,7 @@ void create_copy_bind_group() { .entryCount = bindGroupEntries.size(), .entries = bindGroupEntries.data(), }; - g_CopyBindGroup = g_device.CreateBindGroup(&bindGroupDescriptor); + return g_device.CreateBindGroup(&bindGroupDescriptor); } static wgpu::BackendType to_wgpu_backend(AuroraBackend backend) { @@ -668,7 +699,7 @@ void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, ui g_frameBuffer = create_render_texture(width, height, true); g_frameBufferResolved = create_render_texture(width, height, false); g_depthBuffer = create_depth_texture(width, height); - create_copy_bind_group(); + g_CopyBindGroup = create_copy_bind_group(present_source()); } } // namespace aurora::webgpu diff --git a/lib/webgpu/gpu.hpp b/lib/webgpu/gpu.hpp index e7d5945..7dfedb2 100644 --- a/lib/webgpu/gpu.hpp +++ b/lib/webgpu/gpu.hpp @@ -24,6 +24,20 @@ struct TextureWithSampler { wgpu::TextureFormat format; wgpu::Sampler sampler; }; +struct Viewport { + float left; + float top; + float width; + float height; + float znear; + float zfar; + + bool operator==(const Viewport& rhs) const { + return left == rhs.left && top == rhs.top && width == rhs.width && height == rhs.height && znear == rhs.znear && + zfar == rhs.zfar; + } + bool operator!=(const Viewport& rhs) const { return !(*this == rhs); } +}; extern wgpu::Device g_device; extern wgpu::Queue g_queue; @@ -40,13 +54,18 @@ extern wgpu::Instance g_instance; bool initialize(AuroraBackend backend); void shutdown(); bool refresh_surface(bool recreate = true); -void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, uint32_t native_height, bool force = false); +void resize_swapchain(uint32_t width, uint32_t height, uint32_t native_width, uint32_t native_height, + bool force = false); TextureWithSampler create_render_texture(uint32_t width, uint32_t height, bool multisampled); +const TextureWithSampler& present_source() noexcept; +wgpu::BindGroup create_copy_bind_group(const TextureWithSampler& source); +Viewport calculate_present_viewport(uint32_t surface_width, uint32_t surface_height, uint32_t content_width, + uint32_t content_height) noexcept; void draw_clear(const wgpu::RenderPassEncoder& pass, bool clearColor, bool clearAlpha, bool clearDepth, const Vec4& clearColorValue, float clearDepthValue); -size_t load_from_cache(void const * key, size_t keySize, void * value, size_t valueSize, void * userdata); -void store_to_cache(void const * key, size_t keySize, void const * value, size_t valueSize, void * userdata); +size_t load_from_cache(void const* key, size_t keySize, void* value, size_t valueSize, void* userdata); +void store_to_cache(void const* key, size_t keySize, void const* value, size_t valueSize, void* userdata); void cache_shutdown(); } // namespace aurora::webgpu diff --git a/lib/window.cpp b/lib/window.cpp index b5eb850..63e4af8 100644 --- a/lib/window.cpp +++ b/lib/window.cpp @@ -34,6 +34,7 @@ Module Log("aurora::window"); SDL_Window* g_window; SDL_Renderer* g_renderer; float g_frameBufferScale = 0.f; +bool g_frameBufferAspectFit = false; AuroraWindowSize g_windowSize; std::vector g_events; @@ -64,6 +65,25 @@ Vec2 scale_frame_buffer_to_aspect(int base_width, int base_height, float sc }; } +Vec2 fit_frame_buffer_to_aspect(int width, int height, float aspect) { + if (width <= 0 || height <= 0 || aspect <= 0.f) { + return {std::max(width, 1), std::max(height, 1)}; + } + + const float targetAspect = static_cast(width) / static_cast(height); + if (targetAspect > aspect) { + return { + std::max(1, static_cast(std::lround(static_cast(height) * aspect))), + height, + }; + } + + return { + width, + std::max(1, static_cast(std::lround(static_cast(width) / aspect))), + }; +} + void resize_swapchain() noexcept { const auto size = get_window_size(); if (size == g_windowSize) { @@ -380,6 +400,15 @@ AuroraWindowSize get_window_size() { fb_w = scaledW; fb_h = scaledH; } + if (g_frameBufferAspectFit) { + const auto [baseW, baseH] = vi::configured_fb_size(); + if (baseW > 0 && baseH > 0) { + const auto [fitW, fitH] = + fit_frame_buffer_to_aspect(fb_w, fb_h, static_cast(baseW) / static_cast(baseH)); + fb_w = fitW; + fb_h = fitH; + } + } const float scale = SDL_GetWindowDisplayScale(g_window); return { @@ -426,6 +455,13 @@ static void push_future_resize_event() { TRY_WARN(SDL_PushEvent(&event), "Failed to push SDL event for future resize: {}", SDL_GetError()); } +void request_frame_buffer_resize() { + if (g_window != nullptr) { + // Defer so that we don't try to reconfigure the surface in the middle of game logic. + push_future_resize_event(); + } +} + void set_frame_buffer_scale(float scale) { if (scale < 0.f) { scale = 0.f; @@ -434,10 +470,16 @@ void set_frame_buffer_scale(float scale) { return; } g_frameBufferScale = scale; - if (g_window != nullptr) { - // Defer so that we don't try to reconfigure the surface in the middle of game logic. - push_future_resize_event(); + request_frame_buffer_resize(); +} + +void set_frame_buffer_aspect_fit(bool fit) { + if (g_frameBufferAspectFit == fit) { + return; } + + g_frameBufferAspectFit = fit; + request_frame_buffer_resize(); } } // namespace aurora::window diff --git a/lib/window.hpp b/lib/window.hpp index 187e6a4..b764c1f 100644 --- a/lib/window.hpp +++ b/lib/window.hpp @@ -23,5 +23,7 @@ bool get_fullscreen(); void set_window_size(uint32_t width, uint32_t height); void set_window_position(uint32_t x, uint32_t y); void center_window(); +void request_frame_buffer_resize(); void set_frame_buffer_scale(float scale); +void set_frame_buffer_aspect_fit(bool fit); }; // namespace aurora::window diff --git a/tests/gx_test_stubs.cpp b/tests/gx_test_stubs.cpp index 118d294..785085e 100644 --- a/tests/gx_test_stubs.cpp +++ b/tests/gx_test_stubs.cpp @@ -248,6 +248,7 @@ bool try_bind_replacement(GXTexObj_&, GXTexMapID) noexcept { return false; } #include "../lib/window.hpp" namespace aurora::window { AuroraWindowSize get_window_size() { return {640, 480, 640, 480, 640, 480, 1.0f}; } +void set_frame_buffer_aspect_fit(bool) {} } // namespace aurora::window // --- WebGPU C API stubs (prevent linker errors from wgpu:: destructors) ---