diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d8f5c44..c5977f6 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,4 +1,4 @@ if (AURORA_ENABLE_GX) add_executable(simple simple.c) - target_link_libraries(simple PRIVATE aurora::core aurora::gx aurora::main) + target_link_libraries(simple PRIVATE aurora::core aurora::gx aurora::main aurora::vi) endif () diff --git a/include/dolphin/gx/GXAurora.h b/include/dolphin/gx/GXAurora.h index e464998..6c44287 100644 --- a/include/dolphin/gx/GXAurora.h +++ b/include/dolphin/gx/GXAurora.h @@ -11,6 +11,18 @@ extern "C" { // Subcommands for GX_LOAD_AURORA. // +/** + * Sets the actual render viewport in native framebuffer coordinates. + * Must be followed by six f32 values: left, top, width, height, nearz, farz. + */ +#define GX_LOAD_AURORA_VIEWPORT_RENDER 0x0001 + +/** + * Sets the actual render scissor in native framebuffer coordinates. + * Must be followed by four u32 values: left, top, width, height. + */ +#define GX_LOAD_AURORA_SCISSOR_RENDER 0x0002 + /** * Aurora equivalent of CP_REG_ARRAYBASE_ID: sets the base address and size of a vertex array. * This command must be followed by a 64-bit memory address, 32-bit size, and 1-byte little-endian flag. @@ -68,6 +80,35 @@ 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 +} AuroraViewportPolicy; + +/** + * Configures how GXSetViewport/GXSetScissor parameters are applied to the actual render viewport. + * 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. + */ +void AuroraGetRenderSize(u32* width, u32* height); + +/** + * Sets the actual render viewport in native framebuffer coordinates. + * Overrides the automatically scaled values set by the logical GXSetViewport. + */ +void GXSetViewportRender(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz); + +/** + * Sets the actual render scissor in native framebuffer coordinates. + * Overrides the automatically scaled values set by the logical GXSetScissor. + */ +void GXSetScissorRender(u32 left, u32 top, u32 wd, u32 ht); + /** * Create an offscreen framebuffer and switch rendering to it. * All subsequent GX rendering will target this framebuffer until GXRestoreFrameBuffer() is called. diff --git a/include/dolphin/vi.h b/include/dolphin/vi.h index b24244e..377d952 100644 --- a/include/dolphin/vi.h +++ b/include/dolphin/vi.h @@ -39,6 +39,12 @@ void VISetWindowSize(uint32_t width, uint32_t height); void VISetWindowPosition(uint32_t x, uint32_t y); void VICenterWindow(); +/** + * Sets the internal framebuffer resolution to a specific scale factor of the configured EFB size. + * A value of 0.0f means "Auto", which will use the underlying swapchain size (usually the window size). + */ +void VISetFrameBufferScale(float scale); + /** * \brief Lock the GX framebuffer to a specific aspect ratio, without changing the native framebuffer. * diff --git a/lib/aurora.cpp b/lib/aurora.cpp index adb7bd2..c10bcfc 100644 --- a/lib/aurora.cpp +++ b/lib/aurora.cpp @@ -64,6 +64,41 @@ constexpr std::array PreferredBackendOrder{}; bool g_initialFrame = false; +#ifdef AURORA_ENABLE_GX +struct PresentViewport { + float x; + float y; + float width; + float height; +}; + +PresentViewport 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 { + .x = static_cast((surface_width - viewport_width) / 2), + .y = static_cast((surface_height - viewport_height) / 2), + .width = static_cast(viewport_width), + .height = static_cast(viewport_height), + }; +} +#endif + AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexcept { g_config = config; Log.info("Aurora initializing"); @@ -149,10 +184,7 @@ AuroraInfo initialize(int argc, char* argv[], const AuroraConfig& config) noexce }; } -void aurora_set_log_level(AuroraLogLevel level) noexcept { - g_config.logLevel = level; -} - +void aurora_set_log_level(AuroraLogLevel level) noexcept { g_config.logLevel = level; } #ifdef AURORA_ENABLE_GX wgpu::TextureView g_currentView; @@ -244,13 +276,10 @@ void end_frame() noexcept { pass.SetPipeline(webgpu::g_CopyPipeline); pass.SetBindGroup(0, webgpu::g_CopyBindGroup, 0, nullptr); - // Center viewport to framebuffer size in case we're at an aspect ratio lock. - { - uint32_t pos_x = (webgpu::g_graphicsConfig.surfaceConfiguration.width - webgpu::g_frameBuffer.size.width) / 2; - uint32_t pos_y = (webgpu::g_graphicsConfig.surfaceConfiguration.height - webgpu::g_frameBuffer.size.height) / 2; - - pass.SetViewport(pos_x, pos_y, webgpu::g_frameBuffer.size.width, webgpu::g_frameBuffer.size.height, 0, 1); - } + 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); + pass.SetViewport(viewport.x, viewport.y, viewport.width, viewport.height, 0, 1); pass.Draw(3); imgui::render(pass); diff --git a/lib/dolphin/gx/GXAurora.cpp b/lib/dolphin/gx/GXAurora.cpp index e58f7f9..e71b481 100644 --- a/lib/dolphin/gx/GXAurora.cpp +++ b/lib/dolphin/gx/GXAurora.cpp @@ -1,12 +1,16 @@ #include "dolphin/gx/GXAurora.h" +#include +#include #include #include "__gx.h" #include "gx.hpp" +#include "../../window.hpp" #include "../../gfx/common.hpp" #include "../../gx/fifo.hpp" +#include "../vi/vi_internal.hpp" static void GXWriteString(const char* label) { auto length = strlen(label); @@ -25,15 +29,61 @@ void GXPushDebugGroup(const char* label) { GXWriteString(label); } -void GXPopDebugGroup() { - GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_GROUP_POP); -} +void GXPopDebugGroup() { GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_GROUP_POP); } void GXInsertDebugMarker(const char* label) { GX_WRITE_AURORA(GX_LOAD_AURORA_DEBUG_MARKER_INSERT); GXWriteString(label); } +void AuroraSetViewportPolicy(AuroraViewportPolicy policy) { g_gxState.viewportPolicy = policy; } + +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; + } + if (height != nullptr) { + *height = renderHeight; + } +} + +void GXSetViewportRender(f32 left, f32 top, f32 wd, f32 ht, f32 nearz, f32 farz) { + GX_WRITE_AURORA(GX_LOAD_AURORA_VIEWPORT_RENDER); + GX_WRITE_F32(left); + GX_WRITE_F32(top); + GX_WRITE_F32(wd); + GX_WRITE_F32(ht); + GX_WRITE_F32(nearz); + GX_WRITE_F32(farz); +} + +void GXSetScissorRender(u32 left, u32 top, u32 wd, u32 ht) { + GX_WRITE_AURORA(GX_LOAD_AURORA_SCISSOR_RENDER); + GX_WRITE_U32(left); + GX_WRITE_U32(top); + GX_WRITE_U32(wd); + GX_WRITE_U32(ht); +} + void GXCreateFrameBuffer(u32 width, u32 height) { aurora::gx::fifo::drain(); aurora::gfx::begin_offscreen(width, height); diff --git a/lib/dolphin/gx/GXCull.cpp b/lib/dolphin/gx/GXCull.cpp index e2aba62..7e01fce 100644 --- a/lib/dolphin/gx/GXCull.cpp +++ b/lib/dolphin/gx/GXCull.cpp @@ -4,22 +4,19 @@ extern "C" { void GXSetScissor(u32 left, u32 top, u32 width, u32 height) { - const u32 tp = top + 342; - const u32 lf = left + 342; + const u32 tp = top + 340; + const u32 lf = left + 340; const u32 bm = tp + height - 1; const u32 rt = lf + width - 1; - // NOTE: changed bit size from 11 to 16 to accommodate for higher resolutions - SET_REG_FIELD(0, __gx->suScis0, 16, 0, tp); - SET_REG_FIELD(0, __gx->suScis0, 16, 16, lf); - SET_REG_FIELD(0, __gx->suScis1, 16, 0, bm); - SET_REG_FIELD(0, __gx->suScis1, 16, 16, rt); + SET_REG_FIELD(0, __gx->suScis0, 11, 0, tp); + SET_REG_FIELD(0, __gx->suScis0, 11, 12, lf); + SET_REG_FIELD(0, __gx->suScis1, 11, 0, bm); + SET_REG_FIELD(0, __gx->suScis1, 11, 12, rt); - // GX_WRITE_RAS_REG(__gx->suScis0); - // GX_WRITE_RAS_REG(__gx->suScis1); - // __gx->bpSent = 1; - - aurora::gfx::set_scissor(left, top, width, height); + GX_WRITE_RAS_REG(__gx->suScis0); + GX_WRITE_RAS_REG(__gx->suScis1); + __gx->bpSent = 1; } // TODO GXSetScissorBoxOffset diff --git a/lib/dolphin/gx/GXFrameBuffer.cpp b/lib/dolphin/gx/GXFrameBuffer.cpp index b53a9f5..102e40b 100644 --- a/lib/dolphin/gx/GXFrameBuffer.cpp +++ b/lib/dolphin/gx/GXFrameBuffer.cpp @@ -5,8 +5,31 @@ #include "../../gfx/texture.hpp" #include "../../window.hpp" #include "../../gfx/clear.hpp" -#include "../../webgpu/wgpu.hpp" #include "../../webgpu/gpu.hpp" +#include "../vi/vi_internal.hpp" + +#include +#include + +namespace { +aurora::Vec2 scale_copy_dst(u32 logicalWidth, u32 logicalHeight) { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return {logicalWidth, logicalHeight}; + } + + const auto [logicalFbWidth, logicalFbHeight] = aurora::gx::logical_fb_size(); + const auto [targetWidth, targetHeight] = aurora::gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return {logicalWidth, logicalHeight}; + } + + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); + const auto scaledWidth = std::max(static_cast(std::lround(static_cast(logicalWidth) * scaleX)), 1); + const auto scaledHeight = std::max(static_cast(std::lround(static_cast(logicalHeight) * scaleY)), 1); + return {scaledWidth, scaledHeight}; +} +} // namespace extern "C" { GXRenderModeObj GXNtsc480IntDf = { @@ -125,9 +148,8 @@ void GXSetDispCopyGamma(GXGamma gamma) {} void GXCopyDisp(void* dest, GXBool clear) {} void GXCopyTex(void* dest, GXBool clear) { - const auto& rect = g_gxState.texCopySrc; - const u32 dstWidth = g_gxState.texCopyDstWidth; - const u32 dstHeight = g_gxState.texCopyDstHeight; + const auto rect = aurora::gx::map_logical_scissor(g_gxState.texCopySrc); + const auto [dstWidth, dstHeight] = scale_copy_dst(g_gxState.texCopyDstWidth, g_gxState.texCopyDstHeight); const auto texCopyFmt = g_gxState.texCopyFmt; const aurora::gx::GXState::CopyTextureKey key{ diff --git a/lib/dolphin/gx/GXGet.cpp b/lib/dolphin/gx/GXGet.cpp index c77920a..289f9ca 100644 --- a/lib/dolphin/gx/GXGet.cpp +++ b/lib/dolphin/gx/GXGet.cpp @@ -238,14 +238,13 @@ void GXGetProjectionv(f32* p) { void GXGetScissor(u32* left, u32* top, u32* wd, u32* ht) { CHECK(left != nullptr && top != nullptr && wd != nullptr && ht != nullptr, "null scissor output"); - // NOTE: changed bit size from 11 to 16 to accommodate for higher resolutions - const u32 tp = GET_REG_FIELD(__gx->suScis0, 16, 0); - const u32 lf = GET_REG_FIELD(__gx->suScis0, 16, 16); - const u32 bm = GET_REG_FIELD(__gx->suScis1, 16, 0); - const u32 rt = GET_REG_FIELD(__gx->suScis1, 16, 16); + const u32 tp = GET_REG_FIELD(__gx->suScis0, 11, 0); + const u32 lf = GET_REG_FIELD(__gx->suScis0, 11, 12); + const u32 bm = GET_REG_FIELD(__gx->suScis1, 11, 0); + const u32 rt = GET_REG_FIELD(__gx->suScis1, 11, 12); - *left = lf - 342; - *top = tp - 342; + *left = lf - 340; + *top = tp - 340; *wd = rt - lf + 1; *ht = bm - tp + 1; } diff --git a/lib/dolphin/gx/GXTransform.cpp b/lib/dolphin/gx/GXTransform.cpp index af72632..5f97ad8 100644 --- a/lib/dolphin/gx/GXTransform.cpp +++ b/lib/dolphin/gx/GXTransform.cpp @@ -117,10 +117,28 @@ void GXSetViewport(float left, float top, float width, float height, float nearZ } void GXSetViewportJitter(float left, float top, float width, float height, float nearZ, float farZ, u32 field) { + float sx; + float sy; + float sz; + float ox; + float oy; + float oz; + float zmin; + float zmax; + if (field == 0) { top -= 0.5f; } + sx = width / 2.0f; + sy = -height / 2.0f; + ox = 340.0f + (left + width / 2.0f); + oy = 340.0f + (top + height / 2.0f); + zmin = 1.6777215e7f * nearZ; + zmax = 1.6777215e7f * farZ; + sz = zmax - zmin; + oz = zmax; + __gx->vpLeft = left; __gx->vpTop = top; __gx->vpWd = width; @@ -128,8 +146,15 @@ void GXSetViewportJitter(float left, float top, float width, float height, float __gx->vpNearz = nearZ; __gx->vpFarz = farZ; - // TODO: custom aurora FIFO command - aurora::gfx::set_viewport(left, top, width, height, nearZ, farZ); + GX_WRITE_U8(0x10); + GX_WRITE_U32(0x0005101A); + GX_WRITE_XF_REG_F(26, sx); + GX_WRITE_XF_REG_F(27, sy); + GX_WRITE_XF_REG_F(28, sz); + GX_WRITE_XF_REG_F(29, ox); + GX_WRITE_XF_REG_F(30, oy); + GX_WRITE_XF_REG_F(31, oz); + __gx->bpSent = 0; } void GXProject(f32 x, f32 y, f32 z, const f32 mtx[3][4], const f32* pm, const f32* vp, f32* sx, diff --git a/lib/dolphin/vi/vi.cpp b/lib/dolphin/vi/vi.cpp index d2663a8..7530fa0 100644 --- a/lib/dolphin/vi/vi.cpp +++ b/lib/dolphin/vi/vi.cpp @@ -1,9 +1,39 @@ #include #include "../../window.hpp" +#include "aurora/math.hpp" +#include "vi_internal.hpp" + +#include + +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 { + if (!g_renderMode) { + return {640, 480}; + } + return {g_renderMode->fbWidth, g_renderMode->efbHeight}; +} +} // namespace aurora::vi extern "C" { void VIInit() {} +void VIConfigure(const GXRenderModeObj* rm) { aurora::vi::configure(rm); } +void VIConfigurePan(u16 xOrg, u16 yOrg, u16 width, u16 height) { + (void)xOrg; + (void)yOrg; + (void)width; + (void)height; +} u32 VIGetTvFormat() { return 0; } void VIFlush() {} @@ -13,6 +43,5 @@ bool VIGetWindowFullscreen() { return aurora::window::get_fullscreen(); } void VISetWindowSize(uint32_t width, uint32_t height) { aurora::window::set_window_size(width, height); } void VISetWindowPosition(uint32_t x, uint32_t y) { aurora::window::set_window_position(x, y); } void VICenterWindow() { aurora::window::center_window(); } -void VILockAspectRatio(int width, int height) { return aurora::window::lock_aspect_ratio(width, height); } -void VIUnlockAspectRatio() { return aurora::window::unlock_aspect_ratio(); } +void VISetFrameBufferScale(float scale) { aurora::window::set_frame_buffer_scale(scale); } } diff --git a/lib/dolphin/vi/vi_internal.hpp b/lib/dolphin/vi/vi_internal.hpp new file mode 100644 index 0000000..a711c3f --- /dev/null +++ b/lib/dolphin/vi/vi_internal.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include + +#include + +namespace aurora::vi { +void configure(const GXRenderModeObj* rm) noexcept; +Vec2 configured_fb_size() noexcept; +} // namespace aurora::vi diff --git a/lib/gfx/common.cpp b/lib/gfx/common.cpp index 8d7b974..bd4c00d 100644 --- a/lib/gfx/common.cpp +++ b/lib/gfx/common.cpp @@ -9,6 +9,7 @@ #include "tex_palette_conv.hpp" #include "texture_replacement.hpp" #include "texture.hpp" +#include "../window.hpp" #include #include @@ -46,15 +47,6 @@ enum class CommandType { Draw, DebugMarker, }; -struct SetScissorCommand { - uint32_t x; - uint32_t y; - uint32_t w; - uint32_t h; - - bool operator==(const SetScissorCommand& rhs) const { return x == rhs.x && y == rhs.y && w == rhs.w && h == rhs.h; } - bool operator!=(const SetScissorCommand& rhs) const { return !(*this == rhs); } -}; struct Command { CommandType type; #ifdef AURORA_GFX_DEBUG_GROUPS @@ -62,7 +54,7 @@ struct Command { #endif union Data { Viewport setViewport; - SetScissorCommand setScissor; + ClipRect setScissor; ShaderDrawCommand draw; size_t debugMarkerIndex; } data; @@ -149,7 +141,7 @@ static u32 g_currentRenderPass = UINT32_MAX; static bool g_inOffscreen = false; static std::optional g_suspendedEfbPass; static Viewport g_suspendedEfbViewport; -static SetScissorCommand g_suspendedEfbScissor; +static ClipRect g_suspendedEfbScissor; static webgpu::TextureWithSampler g_offscreenColor; static webgpu::TextureWithSampler g_offscreenDepth; @@ -214,20 +206,25 @@ static void push_draw_command(ShaderDrawCommand data) { ++g_stats.drawCallCount; } -static Viewport g_cachedViewport; -const Viewport& get_viewport() noexcept { return g_cachedViewport; } +Vec2 get_render_target_size() noexcept { + if (g_currentRenderPass < g_renderPasses.size()) { + const auto& size = g_renderPasses[g_currentRenderPass].targetSize; + return {size.width, size.height}; + } + const auto windowSize = window::get_window_size(); + return {windowSize.fb_width, windowSize.fb_height}; +} -void set_viewport(float left, float top, float width, float height, float znear, float zfar) noexcept { - Viewport cmd{left, top, width, height, znear, zfar}; +static Viewport g_cachedViewport; +void set_viewport(const Viewport& cmd) noexcept { if (cmd != g_cachedViewport) { push_command(CommandType::SetViewport, Command::Data{.setViewport = cmd}); g_cachedViewport = cmd; } } -static SetScissorCommand g_cachedScissor; -void set_scissor(uint32_t x, uint32_t y, uint32_t w, uint32_t h) noexcept { - SetScissorCommand cmd{x, y, w, h}; +static ClipRect g_cachedScissor; +void set_scissor(const ClipRect& cmd) noexcept { if (cmd != g_cachedScissor) { push_command(CommandType::SetScissor, Command::Data{.setScissor = cmd}); g_cachedScissor = cmd; @@ -407,7 +404,7 @@ void begin_offscreen(uint32_t width, uint32_t height) { g_inOffscreen = true; g_cachedViewport = {0.f, 0.f, static_cast(width), static_cast(height), 0.f, 1.f}; - g_cachedScissor = {0, 0, width, height}; + g_cachedScissor = {0, 0, static_cast(width), static_cast(height)}; push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } @@ -647,6 +644,9 @@ void begin_frame() { g_renderPasses[0].clearColorValue = gx::g_gxState.clearColor; g_renderPasses[0].clearDepthValue = gx::clear_depth_value(); g_currentRenderPass = 0; + // Refresh render viewport/scissor from logical in case FB size changed + g_cachedViewport = gx::map_logical_viewport(gx::g_gxState.logicalViewport); + g_cachedScissor = gx::map_logical_scissor(gx::g_gxState.logicalScissor); push_command(CommandType::SetViewport, Command::Data{.setViewport = g_cachedViewport}); push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); begin_pipeline_frame(); @@ -857,10 +857,10 @@ void render_pass(const wgpu::RenderPassEncoder& pass, u32 idx) { case CommandType::SetScissor: { const auto& sc = cmd.data.setScissor; const auto& size = g_renderPasses[idx].targetSize; - const auto x = std::clamp(sc.x, 0u, size.width); - const auto y = std::clamp(sc.y, 0u, size.height); - const auto w = std::clamp(sc.w, 0u, size.width - x); - const auto h = std::clamp(sc.h, 0u, size.height - y); + const auto x = std::clamp(static_cast(sc.x), 0u, size.width); + const auto y = std::clamp(static_cast(sc.y), 0u, size.height); + const auto w = std::clamp(static_cast(sc.width), 0u, size.width - x); + const auto h = std::clamp(static_cast(sc.height), 0u, size.height - y); pass.SetScissorRect(x, y, w, h); } break; case CommandType::Draw: { diff --git a/lib/gfx/common.hpp b/lib/gfx/common.hpp index feb8909..aaac9b7 100644 --- a/lib/gfx/common.hpp +++ b/lib/gfx/common.hpp @@ -295,9 +295,9 @@ wgpu::Sampler& sampler_ref(const wgpu::SamplerDescriptor& descriptor); uint32_t align_uniform(uint32_t value); -const Viewport& get_viewport() noexcept; -void set_viewport(float left, float top, float width, float height, float znear, float zfar) noexcept; -void set_scissor(uint32_t x, uint32_t y, uint32_t w, uint32_t h) noexcept; +Vec2 get_render_target_size() noexcept; +void set_viewport(const Viewport& viewport) noexcept; +void set_scissor(const ClipRect& scissor) noexcept; void push_debug_group(std::string label); void insert_debug_marker(std::string label); diff --git a/lib/gx/command_processor.cpp b/lib/gx/command_processor.cpp index 1ad1b86..c0934b5 100644 --- a/lib/gx/command_processor.cpp +++ b/lib/gx/command_processor.cpp @@ -632,9 +632,15 @@ static void handle_bp(u32 value, bool bigEndian) { // Scissor registers (0x20, 0x21) case 0x20: case 0x21: { -#ifndef NDEBUG - Log.debug("Unimplemented: BP register {:x} (scissor)", regId); -#endif + const u32 scis0 = g_gxState.bpRegCache[0x20]; + const u32 scis1 = g_gxState.bpRegCache[0x21]; + const int32_t tp = static_cast(bp_get(scis0, 11, 0)); + const int32_t lf = static_cast(bp_get(scis0, 11, 12)); + const int32_t bm = static_cast(bp_get(scis1, 11, 0)); + const int32_t rt = static_cast(bp_get(scis1, 11, 12)); + if (rt >= lf && bm >= tp) { + set_logical_scissor({lf - 340, tp - 340, rt - lf + 1, bm - tp + 1}); + } break; } @@ -1380,11 +1386,14 @@ static void handle_xf(const u8* data, u32& pos, u32 size, bool bigEndian) { f32 oz = read_f32(xfData + 20, bigEndian); f32 width = sx * 2.0f; f32 height = -sy * 2.0f; - f32 left = ox - 340.0f - width / 2.0f; - f32 top = oy - 340.0f - height / 2.0f; - f32 farZ = oz / 1.6777215e7f; - f32 nearZ = (oz - sz) / 1.6777215e7f; - gfx::set_viewport(left, top, width, height, nearZ, farZ); + set_logical_viewport({ + .left = ox - 340.0f - width / 2.0f, + .top = oy - 340.0f - height / 2.0f, + .width = width, + .height = height, + .znear = (oz - sz) / 1.6777215e7f, + .zfar = oz / 1.6777215e7f, + }); } break; } @@ -1660,7 +1669,40 @@ void handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { pos += 2; // Setting of vertex array bases. - if (subCmd >= GX_LOAD_AURORA_ARRAYBASE && subCmd <= (GX_LOAD_AURORA_ARRAYBASE | 0x0f)) { + if (subCmd == GX_LOAD_AURORA_VIEWPORT_RENDER) { + CHECK(pos + 24 <= size, "GX_LOAD_AURORA_VIEWPORT_RENDER read overrun"); + const f32 left = read_f32(data + pos, bigEndian); + pos += 4; + const f32 top = read_f32(data + pos, bigEndian); + pos += 4; + const f32 width = read_f32(data + pos, bigEndian); + pos += 4; + const f32 height = read_f32(data + pos, bigEndian); + pos += 4; + const f32 nearZ = read_f32(data + pos, bigEndian); + pos += 4; + const f32 farZ = read_f32(data + pos, bigEndian); + pos += 4; + set_render_viewport({ + .left = left, + .top = top, + .width = width, + .height = height, + .znear = nearZ, + .zfar = farZ, + }); + } else if (subCmd == GX_LOAD_AURORA_SCISSOR_RENDER) { + CHECK(pos + 16 <= size, "GX_LOAD_AURORA_SCISSOR_RENDER read overrun"); + const int32_t left = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + const int32_t top = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + const int32_t width = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + const int32_t height = static_cast(read_u32(data + pos, bigEndian)); + pos += 4; + set_render_scissor({left, top, width, height}); + } else if (subCmd >= GX_LOAD_AURORA_ARRAYBASE && subCmd <= (GX_LOAD_AURORA_ARRAYBASE | 0x0f)) { CHECK(pos + 13 <= size, "GX_LOAD_AURORA_ARRAYBASE read overrun"); u32 attrIdx = subCmd - GX_LOAD_AURORA_ARRAYBASE + GX_VA_POS; diff --git a/lib/gx/gx.cpp b/lib/gx/gx.cpp index abc21f2..694729d 100644 --- a/lib/gx/gx.cpp +++ b/lib/gx/gx.cpp @@ -1,6 +1,7 @@ #include "gx.hpp" #include "pipeline.hpp" +#include "../dolphin/vi/vi_internal.hpp" #include "../webgpu/gpu.hpp" #include "../internal.hpp" #include "../gfx/common.hpp" @@ -241,6 +242,103 @@ u32 resolved_format_for_handle(const gfx::TextureHandle& handle) { } } // namespace +Vec2 logical_fb_size() noexcept { + return gfx::is_offscreen() ? gfx::get_render_target_size() : vi::configured_fb_size(); +} + +gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return logicalViewport; + } + + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return logicalViewport; + } + + const bool stretch = g_gxState.viewportPolicy == AURORA_VIEWPORT_STRETCH; + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); + const float scale = std::min(scaleX, scaleY); + const float xOffset = + stretch ? 0.f : (static_cast(targetWidth) - static_cast(logicalFbWidth) * scale) * 0.5f; + const float yOffset = + stretch ? 0.f : (static_cast(targetHeight) - static_cast(logicalFbHeight) * scale) * 0.5f; + const float mappedScaleX = stretch ? scaleX : scale; + const float mappedScaleY = stretch ? scaleY : scale; + return { + .left = xOffset + logicalViewport.left * mappedScaleX, + .top = yOffset + logicalViewport.top * mappedScaleY, + .width = logicalViewport.width * mappedScaleX, + .height = logicalViewport.height * mappedScaleY, + .znear = logicalViewport.znear, + .zfar = logicalViewport.zfar, + }; +} + +gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept { + if (g_gxState.viewportPolicy == AURORA_VIEWPORT_NATIVE) { + return logicalScissor; + } + + const auto [logicalFbWidth, logicalFbHeight] = logical_fb_size(); + const auto [targetWidth, targetHeight] = gfx::get_render_target_size(); + if (logicalFbWidth == 0 || logicalFbHeight == 0 || targetWidth == 0 || targetHeight == 0) { + return logicalScissor; + } + + const bool stretch = g_gxState.viewportPolicy == AURORA_VIEWPORT_STRETCH; + const float scaleX = static_cast(targetWidth) / static_cast(logicalFbWidth); + const float scaleY = static_cast(targetHeight) / static_cast(logicalFbHeight); + const float scale = std::min(scaleX, scaleY); + const float xOffset = + stretch ? 0.f : (static_cast(targetWidth) - static_cast(logicalFbWidth) * scale) * 0.5f; + const float yOffset = + stretch ? 0.f : (static_cast(targetHeight) - static_cast(logicalFbHeight) * scale) * 0.5f; + const float mappedScaleX = stretch ? scaleX : scale; + const float mappedScaleY = stretch ? scaleY : scale; + + const float left = xOffset + static_cast(logicalScissor.x) * mappedScaleX; + const float top = yOffset + static_cast(logicalScissor.y) * mappedScaleY; + const float right = xOffset + static_cast(logicalScissor.x + logicalScissor.width) * mappedScaleX; + const float bottom = yOffset + static_cast(logicalScissor.y + logicalScissor.height) * mappedScaleY; + + 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)); + const auto mappedRight = + std::clamp(static_cast(std::ceil(right)), mappedLeft, static_cast(targetWidth)); + const auto mappedBottom = + std::clamp(static_cast(std::ceil(bottom)), mappedTop, static_cast(targetHeight)); + + return { + .x = mappedLeft, + .y = mappedTop, + .width = mappedRight - mappedLeft, + .height = mappedBottom - mappedTop, + }; +} + +void set_logical_viewport(const gfx::Viewport& viewport) noexcept { + g_gxState.logicalViewport = viewport; + set_render_viewport(map_logical_viewport(viewport)); +} + +void set_render_viewport(const gfx::Viewport& viewport) noexcept { + g_gxState.renderViewport = viewport; + gfx::set_viewport(viewport); +} + +void set_logical_scissor(const gfx::ClipRect& scissor) noexcept { + g_gxState.logicalScissor = scissor; + set_render_scissor(map_logical_scissor(g_gxState.logicalScissor)); +} + +void set_render_scissor(const gfx::ClipRect& scissor) noexcept { + g_gxState.renderScissor = scissor; + gfx::set_scissor(scissor); +} + const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.textures[static_cast(id)]; } void evict_texture_object(u32 texObjId) noexcept { diff --git a/lib/gx/gx.hpp b/lib/gx/gx.hpp index ded46a6..6bff2c1 100644 --- a/lib/gx/gx.hpp +++ b/lib/gx/gx.hpp @@ -321,6 +321,11 @@ struct GXState { std::array textures; std::array loadedTextures; std::array loadedTluts; + AuroraViewportPolicy viewportPolicy = AURORA_VIEWPORT_FIT; + gfx::Viewport logicalViewport{0.f, 0.f, 640.f, 480.f, 0.f, 1.f}; + gfx::Viewport renderViewport{0.f, 0.f, 640.f, 480.f, 0.f, 1.f}; + gfx::ClipRect logicalScissor{0, 0, 640, 480}; + gfx::ClipRect renderScissor{0, 0, 640, 480}; std::array, MaxTexMtx> texMtxs; std::array, MaxPTTexMtx> ptTexMtxs; std::array tcgs; @@ -385,6 +390,13 @@ void shutdown() noexcept; void clear_copy_texture_cache() noexcept; void evict_texture_object(u32 texObjId) noexcept; void evict_tlut_object(u32 tlutObjId) noexcept; +Vec2 logical_fb_size() noexcept; +gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept; +gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept; +void set_logical_viewport(const gfx::Viewport& viewport) noexcept; +void set_render_viewport(const gfx::Viewport& viewport) noexcept; +void set_logical_scissor(const gfx::ClipRect& scissor) noexcept; +void set_render_scissor(const gfx::ClipRect& scissor) noexcept; const gfx::TextureBind& get_texture(GXTexMapID id) noexcept; void resolve_sampled_textures(const ShaderInfo& info) noexcept; diff --git a/lib/gx/shader.cpp b/lib/gx/shader.cpp index e882e17..3edef26 100644 --- a/lib/gx/shader.cpp +++ b/lib/gx/shader.cpp @@ -828,11 +828,12 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept { // GX_POINTS: expand single vertex to axis-aligned screen-space square vtxXfrAttrsPre += "\n let clip = vec4f(mv_pos, 1.0) * ubuf.proj;" - "\n let point_size = ubuf.line_width * (ubuf.viewport_size.y / 528.0);" + "\n let viewport_scale = ubuf.render_viewport_size / max(ubuf.logical_viewport_size, vec2f(1.0));" + "\n let point_size = ubuf.line_width * min(viewport_scale.x, viewport_scale.y);" "\n let x_sign = select(-1.0, 1.0, (vidx & 1u) != 0u);" "\n let y_sign = select(-1.0, 1.0, vidx >= 2u);" "\n let offset_px = vec2f(x_sign, y_sign) * (point_size / 2.0);" - "\n let offset_ndc = (offset_px * 2.0) / ubuf.viewport_size;" + "\n let offset_ndc = (offset_px * 2.0) / ubuf.render_viewport_size;" "\n out.pos = vec4f(clip.xy + offset_ndc * clip.w, clip.zw);"; } else { // GX_LINES / GX_LINESTRIP: expand line segment perpendicular to direction @@ -841,12 +842,13 @@ wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept { "\n let clip_b = vec4f(mv_pos_b, 1.0) * ubuf.proj;" "\n let ndc_a = clip_a.xy / clip_a.w;" "\n let ndc_b = clip_b.xy / clip_b.w;" - "\n let delta_px = (ndc_b - ndc_a) / 2.0 * ubuf.viewport_size;" + "\n let viewport_scale = ubuf.render_viewport_size / max(ubuf.logical_viewport_size, vec2f(1.0));" + "\n let delta_px = (ndc_b - ndc_a) / 2.0 * ubuf.render_viewport_size;" "\n let dir_px = select(vec2f(1.0, 0.0), normalize(delta_px), dot(delta_px, delta_px) > 1e-10);" "\n let perp_px = vec2f(-dir_px.y, dir_px.x);" - "\n let line_width = ubuf.line_width * (ubuf.viewport_size.y / 528.0);" // Scale line width based on viewport + "\n let line_width = ubuf.line_width * min(viewport_scale.x, viewport_scale.y);" "\n let offset_px = perp_px * (line_width / 2.0) * select(-1.0, 1.0, (vidx & 1u) != 0u);" - "\n let offset_ndc = (offset_px * 2.0) / ubuf.viewport_size;" + "\n let offset_ndc = (offset_px * 2.0) / ubuf.render_viewport_size;" "\n let clip_base = select(clip_a, clip_b, use_b);" "\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);"; } @@ -1688,7 +1690,9 @@ fn tev_overflow_vec4f(in: vec4f) -> vec4f {{ struct Uniform {{ vtx_start: u32, current_pnmtx: u32, - viewport_size: vec2f, + render_viewport_size: vec2f, + logical_viewport_size: vec2f, + pad: vec2u, array_start: array,{0} }}; @group(0) @binding(0) diff --git a/lib/gx/shader_info.cpp b/lib/gx/shader_info.cpp index 0e8f3d6..8cf18c2 100644 --- a/lib/gx/shader_info.cpp +++ b/lib/gx/shader_info.cpp @@ -5,6 +5,9 @@ #include namespace aurora::gx { +// TODO: remove, just for testing +bool enableLodBias = true; + namespace { Module Log("aurora::gx"); @@ -13,7 +16,11 @@ bool is_alpha_bump_channel(GXChannelID id) { return id == GX_ALPHA_BUMP || id == Vec4 texture_size_bias(const gfx::TextureBind& tex) { auto width = static_cast(tex.texObj.width()); auto height = static_cast(tex.texObj.height()); - return {width, height, tex.texObj.lod_bias(), 0.0f}; + const auto vpBias = + enableLodBias ? log2(std::min(g_gxState.renderViewport.width / std::max(g_gxState.logicalViewport.width, 1.f), + g_gxState.renderViewport.height / std::max(g_gxState.logicalViewport.height, 1.f))) + : 0.f; + return {width, height, tex.texObj.lod_bias() + vpBias, 0.0f}; } void color_arg_reg_info(GXTevColorArg arg, const TevStage& stage, ShaderInfo& info) { @@ -169,7 +176,8 @@ ShaderInfo build_shader_info(const ShaderConfig& config) noexcept { ZoneScoped; ShaderInfo info{ - .uniformSize = 4 + 4 + 8 + 48 + 64, // vtx_start, current_pnmtx, viewport_size, array_start, proj + // vtx_start, current_pnmtx, render/logical viewport size, array_start, pad, proj + .uniformSize = 4 + 4 + 8 + 8 + 8 + 48 + 64, }; if (config.lineMode != 0) { @@ -343,15 +351,17 @@ static u32 line_texcoord_mask() noexcept { return mask; } -gfx::Range build_uniform(const ShaderInfo& info, u32 vtxStart, - const BindGroupRanges& ranges) noexcept { +gfx::Range build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGroupRanges& ranges) noexcept { ZoneScoped; auto [buf, range] = gfx::map_uniform(info.uniformSize); buf.append(vtxStart); buf.append(g_gxState.currentPnMtx); - buf.append(gfx::get_viewport().width); - buf.append(gfx::get_viewport().height); + buf.append(g_gxState.renderViewport.width); + buf.append(g_gxState.renderViewport.height); + buf.append(g_gxState.logicalViewport.width); + buf.append(g_gxState.logicalViewport.height); + buf.append_zeroes(8); // pad for (const auto& vaRange : ranges.vaRanges) { buf.append(vaRange.offset); } diff --git a/lib/webgpu/gpu.cpp b/lib/webgpu/gpu.cpp index 8979a2e..f5d5355 100644 --- a/lib/webgpu/gpu.cpp +++ b/lib/webgpu/gpu.cpp @@ -591,7 +591,7 @@ bool initialize(AuroraBackend auroraBackend) { .textureAnisotropy = g_config.maxTextureAnisotropy, }; create_copy_pipeline(); - resize_swapchain(size.fb_width, size.fb_height, size.native_fb_width, size.native_fb_width, true); + resize_swapchain(size.fb_width, size.fb_height, size.native_fb_width, size.native_fb_height, true); return true; } diff --git a/lib/window.cpp b/lib/window.cpp index 66bf8a2..73dd7df 100644 --- a/lib/window.cpp +++ b/lib/window.cpp @@ -20,17 +20,19 @@ #include #include -#include +#include +#include #include +#include "dolphin/vi/vi_internal.hpp" + namespace aurora::window { namespace { Module Log("aurora::window"); SDL_Window* g_window; SDL_Renderer* g_renderer; -bool g_aspectRatioLocked; -int g_aspectRatioW, g_aspectRatioH; +float g_frameBufferScale = 0.f; AuroraWindowSize g_windowSize; std::vector g_events; @@ -40,6 +42,27 @@ inline bool operator==(const AuroraWindowSize& lhs, const AuroraWindowSize& rhs) lhs.native_fb_width == rhs.native_fb_width && lhs.scale == rhs.scale; } +Vec2 scale_frame_buffer_to_aspect(int base_width, int base_height, float scale, float aspect) { + if (base_width <= 0 || base_height <= 0 || scale <= 0.f || aspect <= 0.f) { + return {std::max(base_width, 1), std::max(base_height, 1)}; + } + + const int scaled_base_width = std::max(1, static_cast(std::lround(static_cast(base_width) * scale))); + const int scaled_base_height = std::max(1, static_cast(std::lround(static_cast(base_height) * scale))); + const float base_aspect = static_cast(base_width) / static_cast(base_height); + if (aspect >= base_aspect) { + return { + std::max(1, static_cast(std::lround(static_cast(scaled_base_height) * aspect))), + scaled_base_height, + }; + } + + return { + scaled_base_width, + std::max(1, static_cast(std::lround(static_cast(scaled_base_width) / aspect))), + }; +} + void resize_swapchain() noexcept { const auto size = get_window_size(); if (size == g_windowSize) { @@ -344,15 +367,13 @@ AuroraWindowSize get_window_size() { int fb_w = native_fb_w; int fb_h = native_fb_h; - - if (g_aspectRatioLocked) { - // Try as if bounded on width. - fb_h = fb_w * g_aspectRatioH / g_aspectRatioW; - if (fb_h > native_fb_h) { - // Bounded on height instead. - fb_h = native_fb_h; - fb_w = fb_h * g_aspectRatioW / g_aspectRatioH; - } + if (g_frameBufferScale > 0.f) { + const auto [baseW, baseH] = vi::configured_fb_size(); + const auto [scaledW, scaledH] = + scale_frame_buffer_to_aspect(static_cast(baseW), static_cast(baseH), g_frameBufferScale, + static_cast(fb_w) / static_cast(fb_h)); + fb_w = scaledW; + fb_h = scaledH; } const float scale = SDL_GetWindowDisplayScale(g_window); @@ -400,20 +421,18 @@ static void push_future_resize_event() { TRY_WARN(SDL_PushEvent(&event), "Failed to push SDL event for future resize: {}", SDL_GetError()); } -void lock_aspect_ratio(int width, int height) { - g_aspectRatioLocked = true; - g_aspectRatioW = width; - g_aspectRatioH = height; - - // Defer so that we don't try to reconfigure the surface in the middle of game logic. - push_future_resize_event(); -} - -void unlock_aspect_ratio() { - g_aspectRatioLocked = false; - - // 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; + } + if (g_frameBufferScale == 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(); + } } } // namespace aurora::window diff --git a/lib/window.hpp b/lib/window.hpp index 997a78f..187e6a4 100644 --- a/lib/window.hpp +++ b/lib/window.hpp @@ -23,6 +23,5 @@ 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 lock_aspect_ratio(int width, int height); -void unlock_aspect_ratio(); +void set_frame_buffer_scale(float scale); }; // namespace aurora::window diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 988a50b..b3313db 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,6 +29,7 @@ if (AURORA_ENABLE_GX) ../lib/dolphin/gx/GXGet.cpp ../lib/dolphin/gx/GXLighting.cpp ../lib/dolphin/gx/GXManage.cpp + ../lib/dolphin/gx/GXAurora.cpp ../lib/dolphin/gx/GXPerf.cpp ../lib/dolphin/gx/GXPixel.cpp ../lib/dolphin/gx/GXTev.cpp diff --git a/tests/gx_fifo_test.cpp b/tests/gx_fifo_test.cpp index d3c953d..a917dc7 100644 --- a/tests/gx_fifo_test.cpp +++ b/tests/gx_fifo_test.cpp @@ -1963,6 +1963,22 @@ TEST_F(GXFifoTest, GetProjectionAndScissorShadowState) { EXPECT_EQ(height, 240u); } +TEST_F(GXFifoTest, Scissor_EncodesBpAndDecodesLogicalState) { + GXSetScissor(16, 24, 320, 240); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_bp_write(bytes, 0x20)); + EXPECT_TRUE(has_bp_write(bytes, 0x21)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.logicalScissor.x, 16); + EXPECT_EQ(g_gxState.logicalScissor.y, 24); + EXPECT_EQ(g_gxState.logicalScissor.width, 320); + EXPECT_EQ(g_gxState.logicalScissor.height, 240); +} + TEST_F(GXFifoTest, GetViewportShadowState) { f32 vp[6]{}; @@ -1985,6 +2001,53 @@ TEST_F(GXFifoTest, GetViewportShadowState) { EXPECT_FLOAT_EQ(vp[5], 0.9f); } +TEST_F(GXFifoTest, Viewport_DecodesLogicalViewportState) { + GXSetViewport(10.0f, 20.0f, 640.0f, 480.0f, 0.1f, 1.0f); + auto bytes = capture_fifo(); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.left, 10.0f); + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.top, 20.0f); + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.width, 640.0f); + EXPECT_FLOAT_EQ(g_gxState.logicalViewport.height, 480.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.left, 10.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.top, 20.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.width, 640.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.height, 480.0f); +} + +TEST_F(GXFifoTest, ViewportRender_EncodesAuroraOverride) { + GXSetViewportRender(100.0f, 50.0f, 1280.0f, 720.0f, 0.0f, 1.0f); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_VIEWPORT_RENDER)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_FLOAT_EQ(g_gxState.renderViewport.left, 100.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.top, 50.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.width, 1280.0f); + EXPECT_FLOAT_EQ(g_gxState.renderViewport.height, 720.0f); +} + +TEST_F(GXFifoTest, ScissorRender_EncodesAuroraOverride) { + GXSetScissorRender(100, 40, 800, 600); + auto bytes = capture_fifo(); + + EXPECT_TRUE(has_aurora_cmd(bytes, GX_LOAD_AURORA_SCISSOR_RENDER)); + + reset_gx_state(); + decode_fifo(bytes); + + EXPECT_EQ(g_gxState.renderScissor.x, 100); + EXPECT_EQ(g_gxState.renderScissor.y, 40); + EXPECT_EQ(g_gxState.renderScissor.width, 800); + EXPECT_EQ(g_gxState.renderScissor.height, 600); +} + // --- GXLoadLightObjImm (XF 0x600-0x67F) --- TEST_F(GXFifoTest, LoadLightObjImm_Light0_BasicColor) { diff --git a/tests/gx_test_stubs.cpp b/tests/gx_test_stubs.cpp index c13bdd7..1fb5705 100644 --- a/tests/gx_test_stubs.cpp +++ b/tests/gx_test_stubs.cpp @@ -52,6 +52,11 @@ namespace aurora::gx { GXState g_gxState{}; } // namespace aurora::gx +namespace aurora::vi { +Vec2 configured_fb_size() noexcept { return {640, 480}; } +void configure(const GXRenderModeObj*) noexcept {} +} // namespace aurora::vi + // --- Texture uploads --- namespace aurora::gfx { std::vector g_textureUploads; @@ -63,6 +68,19 @@ const gfx::TextureBind& get_texture(GXTexMapID id) noexcept { return g_gxState.t void evict_texture_object(u32) noexcept {} void evict_tlut_object(u32) noexcept {} void shutdown() noexcept {} +Vec2 logical_fb_size() noexcept { return {640, 480}; } +gfx::Viewport map_logical_viewport(const gfx::Viewport& logicalViewport) noexcept { return logicalViewport; } +gfx::ClipRect map_logical_scissor(const gfx::ClipRect& logicalScissor) noexcept { return logicalScissor; } +void set_logical_viewport(const gfx::Viewport& viewport) noexcept { + g_gxState.logicalViewport = viewport; + set_render_viewport(map_logical_viewport(viewport)); +} +void set_render_viewport(const gfx::Viewport& viewport) noexcept { g_gxState.renderViewport = viewport; } +void set_logical_scissor(const gfx::ClipRect& scissor) noexcept { + g_gxState.logicalScissor = scissor; + set_render_scissor(map_logical_scissor(scissor)); +} +void set_render_scissor(const gfx::ClipRect& scissor) noexcept { g_gxState.renderScissor = scissor; } } // namespace aurora::gx // --- Shader/pipeline stubs --- @@ -88,11 +106,8 @@ Range push_indices(const uint8_t* data, size_t length) { return {}; } Range push_uniform(const uint8_t* data, size_t length) { return {}; } Range push_storage(const uint8_t* data, size_t length) { return {}; } -const Viewport& get_viewport() noexcept { - static Viewport vp{0.f, 0.f, 640.f, 480.f, 0.f, 1.f}; - return vp; -} -void set_viewport(float left, float top, float width, float height, float znear, float zfar) noexcept {} +Vec2 get_render_target_size() noexcept { return {640, 480}; } +void set_viewport(const Viewport& viewport) noexcept {} void set_scissor(uint32_t x, uint32_t y, uint32_t w, uint32_t h) noexcept {} } // namespace aurora::gfx @@ -143,6 +158,9 @@ void write_texture(const TextureRef& ref, ArrayRef data) noexcept {} void resolve_pass(TextureHandle texture, ClipRect rect, bool clearColor, bool clearAlpha, bool clearDepth, Vec4 clearColorValue, float clearDepthValue, GXTexFmt resolveFormat) {} void queue_palette_conv(tex_palette_conv::ConvRequest req) {} +void begin_offscreen(uint32_t width, uint32_t height) {} +void end_offscreen() {} +bool is_offscreen() noexcept { return false; } } // namespace aurora::gfx namespace aurora::gfx::tex_copy_conv {