From 0f394212b34d2f567488ad65e9da8b97de9268c2 Mon Sep 17 00:00:00 2001 From: patchzyy <64382339+patchzyy@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:26:31 +0200 Subject: [PATCH] run gx on its own thread --- aurora-main | 2 +- runtime/include/aurora_events.h | 26 +++ runtime/include/gx_worker.h | 112 ++++++++++ runtime/include/runtime_config.h | 8 + runtime/src/hle/audio/audio.cpp | 8 +- runtime/src/hle/audio/ax_dsp.h | 3 + runtime/src/hle/audio/ax_mix.cpp | 12 + runtime/src/hle/gx/gx_copy.cpp | 49 +++-- runtime/src/hle/gx/gx_dl.cpp | 110 ++++++---- runtime/src/hle/gx/gx_egg.cpp | 6 +- runtime/src/hle/gx/gx_fifo.cpp | 14 ++ runtime/src/hle/gx/gx_frame.cpp | 43 ++++ runtime/src/hle/gx/gx_indirect.cpp | 12 +- runtime/src/hle/gx/gx_init.cpp | 33 ++- runtime/src/hle/gx/gx_internal.h | 15 ++ runtime/src/hle/gx/gx_lighting.cpp | 10 +- runtime/src/hle/gx/gx_objects.cpp | 79 +++++-- runtime/src/hle/gx/gx_pixel.cpp | 22 +- runtime/src/hle/gx/gx_stubs.cpp | 32 ++- runtime/src/hle/gx/gx_tev.cpp | 21 +- runtime/src/hle/gx/gx_texture.cpp | 281 +++++++++++++++--------- runtime/src/hle/gx/gx_transform.cpp | 65 ++++-- runtime/src/hle/gx/gx_vertex.cpp | 55 ++++- runtime/src/hle/gx/gx_worker.cpp | 329 ++++++++++++++++++++++++++++ runtime/src/hle/os/os_reset.cpp | 3 + runtime/src/hle/vi.cpp | 7 +- runtime/src/main.cpp | 9 + runtime/src/settings_overlay.cpp | 9 +- 28 files changed, 1137 insertions(+), 238 deletions(-) create mode 100644 runtime/include/gx_worker.h create mode 100644 runtime/src/hle/gx/gx_worker.cpp diff --git a/aurora-main b/aurora-main index 648e77d..78850a8 160000 --- a/aurora-main +++ b/aurora-main @@ -1 +1 @@ -Subproject commit 648e77d7e819b5da8aef90c3178a5a3c6d8cd1d7 +Subproject commit 78850a8d308c7d3f5aaddedea9875c250f25510f diff --git a/runtime/include/aurora_events.h b/runtime/include/aurora_events.h index e10c151..1809487 100644 --- a/runtime/include/aurora_events.h +++ b/runtime/include/aurora_events.h @@ -2,6 +2,7 @@ #include "settings_overlay.h" #include "runtime_config.h" +#include "gx_worker.h" #include #include @@ -131,7 +132,20 @@ inline void ApplyPendingMkwDynamicAspectSurface() { UpdateMkwDynamicAspectSurface(surfaceWidth, surfaceHeight); } +void GxWorkerBeginFrame(); +void GxWorkerEndFrame(AuroraImguiFrame* imgui); + inline bool BeginAuroraFrame() { + if (GxWorker::ShouldDefer()) { + // The window half can touch the surface and render targets the worker draws into. + GxWorker::Sync(); + if (!aurora_begin_frame_window()) { + return false; + } + GxWorker::Submit([] { GxWorkerBeginFrame(); }); + ApplyPendingMkwDynamicAspectSurface(); + return true; + } if (!aurora_begin_frame()) { return false; } @@ -139,6 +153,18 @@ inline bool BeginAuroraFrame() { return true; } +// Ends the frame; with the GX worker, the game thread joins it here so the next frame never +// overlaps guest memory the worker may still read. +inline void EndAuroraFrame() { + if (GxWorker::ShouldDefer()) { + AuroraImguiFrame* imgui = aurora_end_frame_window(); + GxWorker::Submit([imgui] { GxWorkerEndFrame(imgui); }); + GxWorker::Sync(); + return; + } + aurora_end_frame(); +} + // Poll Aurora events and update cached window/framebuffer dimensions. inline void UpdateAuroraAndProcessEvents() { ProcessAuroraEvents(aurora_update()); diff --git a/runtime/include/gx_worker.h b/runtime/include/gx_worker.h new file mode 100644 index 0000000..d5d3515 --- /dev/null +++ b/runtime/include/gx_worker.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Dedicated GX thread (Dolphin "dual core"): the game thread records GX HLE calls in order and +// the worker executes them. While active, GX work runs on the worker except inside inline scopes, +// which run on the game thread after the worker has caught up. +namespace GxWorker { + +namespace detail { +inline constexpr size_t kAlign = 16; + +extern std::atomic g_active; +extern constinit thread_local bool t_onWorker; +extern constinit thread_local int t_inlineDepth; +extern bool g_fifoPending; + +void* Reserve(size_t payloadSize, void (*run)(void*)); +void Commit(); +void FlushFifo(); +} // namespace detail + +// Starts the worker unless [video] gx_worker or MKW_GX_WORKER=0 disables it. Call once Aurora is up. +void Start(); +void Stop(); + +inline bool Active() noexcept { return detail::g_active.load(std::memory_order_relaxed); } +inline bool OnWorker() noexcept { return detail::t_onWorker; } +// True when a GX entry point on this thread must be recorded instead of executed. +inline bool ShouldDefer() noexcept { + return Active() && !detail::t_onWorker && detail::t_inlineDepth == 0; +} + +template +void Submit(F&& fn) { + using Fn = std::decay_t; + static_assert(alignof(Fn) <= detail::kAlign, "over-aligned GX command"); + if (detail::g_fifoPending) { + detail::FlushFifo(); + } + void* storage = detail::Reserve(sizeof(Fn), [](void* p) { + struct Destroy { + Fn* f; + ~Destroy() { f->~Fn(); } + } guard{static_cast(p)}; + (*guard.f)(); + }); + new (storage) Fn(std::forward(fn)); + detail::Commit(); +} + +// Records fn when deferring, otherwise runs it now. Read call-time guest memory before calling. +template +void Run(F&& fn) { + if (ShouldDefer()) { + Submit(std::forward(fn)); + } else { + fn(); + } +} + +// Gather-pipe writes, coalesced into one record per run of consecutive writes and replayed +// with their original sizes. +void FifoWrite(uint32_t value, uint32_t sizeBytes); +void FifoBurst(const uint8_t* data, uint32_t sizeBytes); + +// Called about every millisecond while the game thread waits in Sync. +void SetWaitCallback(void (*callback)()); + +// Blocks until every recorded command has executed. +void Sync(); + +// GX entry points run inline on the game thread while inside (after a Sync), for work whose +// results the guest needs immediately. +void BeginInline(); +void EndInline(); + +class InlineScope { +public: + InlineScope() { BeginInline(); } + ~InlineScope() { EndInline(); } + InlineScope(const InlineScope&) = delete; + InlineScope& operator=(const InlineScope&) = delete; +}; + +} // namespace GxWorker + +// Records the enclosing call for the GX worker and returns, or falls through when it should run +// here (worker thread, inline scope, worker disabled). +#define GX_DEFER_CALL(fn, ...) \ + do { \ + if (GxWorker::ShouldDefer()) { \ + GxWorker::Submit([=] { fn(__VA_ARGS__); }); \ + return; \ + } \ + } while (false) + +#define GX_DEFER_CALL0(fn) \ + do { \ + if (GxWorker::ShouldDefer()) { \ + GxWorker::Submit([] { fn(); }); \ + return; \ + } \ + } while (false) + +// Runs the rest of the enclosing function on this thread once the worker has caught up. +#define GX_SYNC_INLINE() GxWorker::InlineScope gxInlineScope_ diff --git a/runtime/include/runtime_config.h b/runtime/include/runtime_config.h index 055cbec..adafe33 100644 --- a/runtime/include/runtime_config.h +++ b/runtime/include/runtime_config.h @@ -44,6 +44,7 @@ struct RuntimeUserConfig { std::optional frameInterpolationFps; std::optional metalFxSpatialUpscaling; std::optional skipUnreadyPipelines; + std::optional gxWorker; std::optional disableCopyFilter; std::optional textureReplacements; std::optional textureDumps; @@ -275,6 +276,8 @@ inline void EnsureConfigFile() { "display_mode = \"windowed\"\n" "graphics_api = \"auto\"\n" "skip_unready_pipelines = true\n" + "# Runs GX command processing on its own thread. Read once, at startup.\n" + "gx_worker = true\n" "disable_copy_filter = true\n" "show_fps = true\n" "# Dolphin-style custom textures. When enabled, the renderer indexes\n" @@ -432,6 +435,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) { config.metalFxSpatialUpscaling = FindConfigValue(document, "video", "metalfx_spatial_upscaling"); config.skipUnreadyPipelines = FindConfigValue(document, "video", "skip_unready_pipelines"); + config.gxWorker = FindConfigValue(document, "video", "gx_worker"); config.disableCopyFilter = FindConfigValue(document, "video", "disable_copy_filter"); config.showFps = FindConfigValue(document, "video", "show_fps"); config.textureReplacements = FindConfigValue(document, "video", "texture_replacements"); @@ -811,6 +815,10 @@ inline bool AudioMixWorkerEnabled(bool fallback = true) { return Get().audioMixWorker.value_or(fallback); } +inline bool GxWorkerEnabled(bool fallback = true) { + return Get().gxWorker.value_or(fallback); +} + // Whether background music should duck automatically for other media playback. inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) { return Get().attenuateMusicWhenMediaPlays.value_or(fallback); diff --git a/runtime/src/hle/audio/audio.cpp b/runtime/src/hle/audio/audio.cpp index 1be5008..b419fa4 100644 --- a/runtime/src/hle/audio/audio.cpp +++ b/runtime/src/hle/audio/audio.cpp @@ -29,6 +29,8 @@ constexpr uint32_t kAIDmaCallbackAddr = 0x80386480u; // Max completed 3 ms DMA blocks delivered per tick. Draining several at once catches up // backlog from a long frame without letting a large stall spiral into an unbounded loop. constexpr int kMaxBlocksPerTick = 4; +// Backlog (in blocks) tolerated before a catch-up block waits on an in-flight mix. +constexpr double kMaxDeferredBlocks = 2.0; struct AIDmaState { std::mutex mutex; @@ -407,11 +409,15 @@ void Audio_HLE_Tick(CpuContext* ctx, uint32_t deltaMicros) int blocksCompleted = 0; while (true) { + // A block due while the previous mix is still running would block on it; leave it for the + // next service point unless the backlog is getting long. + const bool deferWhileMixing = AxDspHle::MixWorkerBusy(); // Claim the block in one critical section; sample-then-consume separately gains // nothing since the callback below (the only reentrancy point) runs mutex-released. { std::lock_guard lock(g_ai.mutex); - if (g_ai.accumulatorSeconds < blockDuration) { + if (g_ai.accumulatorSeconds < blockDuration || + (deferWhileMixing && g_ai.accumulatorSeconds < blockDuration * kMaxDeferredBlocks)) { break; } g_ai.accumulatorSeconds -= blockDuration; diff --git a/runtime/src/hle/audio/ax_dsp.h b/runtime/src/hle/audio/ax_dsp.h index 8c5f169..1889621 100644 --- a/runtime/src/hle/audio/ax_dsp.h +++ b/runtime/src/hle/audio/ax_dsp.h @@ -25,6 +25,9 @@ void ServiceDeferredCallbacks(); // Cheap when the mix already finished (the normal case). void JoinMixWorker(); +// True while a deferred mix is queued or still running. +bool MixWorkerBusy(); + // Joins and tears down the mix worker thread. Call before the guest memory map // is destroyed or re-created. void ShutdownMixWorker(); diff --git a/runtime/src/hle/audio/ax_mix.cpp b/runtime/src/hle/audio/ax_mix.cpp index ba225e6..1e34694 100644 --- a/runtime/src/hle/audio/ax_mix.cpp +++ b/runtime/src/hle/audio/ax_mix.cpp @@ -112,6 +112,14 @@ public: ConfigureFromTaskLocked(taskPtr); } + bool MixBusy() { + if (!m_mixThread.joinable()) { + return false; + } + std::lock_guard lock(m_mixMutex); + return m_mixPending || m_mixBusy; + } + void JoinMix() { if (m_mixThread.joinable()) { std::unique_lock lock(m_mixMutex); @@ -1560,6 +1568,10 @@ void JoinMixWorker() { Instance().JoinMix(); } +bool MixWorkerBusy() { + return Instance().MixBusy(); +} + void ShutdownMixWorker() { Instance().ShutdownMixWorker(); } diff --git a/runtime/src/hle/gx/gx_copy.cpp b/runtime/src/hle/gx/gx_copy.cpp index 5631a01..3dbdc8d 100644 --- a/runtime/src/hle/gx/gx_copy.cpp +++ b/runtime/src/hle/gx/gx_copy.cpp @@ -71,11 +71,14 @@ void InvalidateEfbCopyDestinationsForRange(uint32_t addr, uint32_t size) { // ============================================================================ extern "C" void GX__SetDispCopySrc_8016f438(uint32_t l, uint32_t t, uint32_t w, uint32_t h) { + GX_DEFER_CALL(GX__SetDispCopySrc_8016f438, l, t, w, h); GXSetDispCopySrc((u16)l, (u16)t, (u16)w, (u16)h); } PPC_NATIVE_OVERRIDE_VOID(8016f438, GX__SetDispCopySrc_8016f438, (uint32_t l, uint32_t t, uint32_t w, uint32_t h), (l, t, w, h)); -extern "C" void GX__SetDispCopyDst_8016f4b8(uint32_t w, uint32_t h) { GXSetDispCopyDst((u16)w, (u16)h); } +extern "C" void GX__SetDispCopyDst_8016f4b8(uint32_t w, uint32_t h) { + GxWorker::Run([w, h] { GXSetDispCopyDst((u16)w, (u16)h); }); +} PPC_NATIVE_OVERRIDE_VOID(8016f4b8, GX__SetDispCopyDst_8016f4b8, (uint32_t w, uint32_t h), (w, h)); // ============================================================================ @@ -83,14 +86,14 @@ PPC_NATIVE_OVERRIDE_VOID(8016f4b8, GX__SetDispCopyDst_8016f4b8, (uint32_t w, uin // ============================================================================ extern "C" void GX__SetTexCopySrc_8016f478(uint32_t l, uint32_t t, uint32_t w, uint32_t h) { - GXSetTexCopySrc((u16)l, (u16)t, (u16)w, (u16)h); + GxWorker::Run([l, t, w, h] { GXSetTexCopySrc((u16)l, (u16)t, (u16)w, (u16)h); }); g_texCopyState.srcLeft=(u16)l; g_texCopyState.srcTop=(u16)t; g_texCopyState.srcWidth=(u16)w; g_texCopyState.srcHeight=(u16)h; } PPC_NATIVE_OVERRIDE_VOID(8016f478, GX__SetTexCopySrc_8016f478, (uint32_t l, uint32_t t, uint32_t w, uint32_t h), (l, t, w, h)); extern "C" void GX__SetTexCopyDst_8016f4dc(uint32_t w, uint32_t h, uint32_t f, uint32_t m) { - GXSetTexCopyDst((u16)w, (u16)h, (GXTexFmt)f, (GXBool)m); + GxWorker::Run([w, h, f, m] { GXSetTexCopyDst((u16)w, (u16)h, (GXTexFmt)f, (GXBool)m); }); g_texCopyState.dstWidth=(u16)w; g_texCopyState.dstHeight=(u16)h; g_texCopyState.dstFormat=f; g_texCopyState.dstMipmap=m; } @@ -100,11 +103,19 @@ extern "C" void GX__SetCopyFilter_8016fa40(uint32_t aa, uint32_t spa, uint32_t v uint8_t sp[12][2]={}, vfb[7]={}; if(spa) std::memcpy(sp, GuestToHostPtr(spa, 24), 24); if(vfa) std::memcpy(vfb, GuestToHostPtr(vfa, 7), 7); - GXSetCopyFilter((GXBool)aa, sp, (GXBool)vf, vfb); + std::array spCopy{}; + std::array vfCopy{}; + std::memcpy(spCopy.data(), sp, 24); + std::memcpy(vfCopy.data(), vfb, 7); + GxWorker::Run([aa, vf, spCopy, vfCopy]() mutable { + GXSetCopyFilter((GXBool)aa, reinterpret_cast(spCopy.data()), (GXBool)vf, vfCopy.data()); + }); } PPC_NATIVE_OVERRIDE_VOID(8016fa40, GX__SetCopyFilter_8016fa40, (uint32_t aa, uint32_t spa, uint32_t vf, uint32_t vfa), (aa, spa, vf, vfa)); -extern "C" void GX__SetDispCopyGamma_8016fc24(uint32_t g) { GXSetDispCopyGamma((GXGamma)g); } +extern "C" void GX__SetDispCopyGamma_8016fc24(uint32_t g) { + GxWorker::Run([g] { GXSetDispCopyGamma((GXGamma)g); }); +} PPC_NATIVE_OVERRIDE_VOID(8016fc24, GX__SetDispCopyGamma_8016fc24, (uint32_t g), (g)); // ============================================================================ @@ -113,18 +124,24 @@ PPC_NATIVE_OVERRIDE_VOID(8016fc24, GX__SetDispCopyGamma_8016fc24, (uint32_t g), extern "C" void GX__CopyDisp_8016fc38(uint32_t da, uint32_t c) { EnsureAuroraFrameActive(); - // GX copies are FIFO-ordered on hardware. Drain submitted draws before - // resolving the EFB so high-level copies see the same contents. - GXDrawDone(); - GXCopyDisp(GuestToHostPtr(da), (GXBool)c); - // No second GXDrawDone here: the frame-worker wait below is for the DONE - // phase, which strictly subsumes the drain this call would perform. + if (GxWorker::ShouldDefer()) { + // The worker drains like the inline path; EndAuroraFrame joins it before the next frame. + GxWorker::Submit([da, c] { + GXDrawDone(); + GXCopyDisp(GuestToHostPtr(da), (GXBool)c); + }); + } else { + // GX copies are FIFO-ordered on hardware. Drain submitted draws before + // resolving the EFB so high-level copies see the same contents. + GXDrawDone(); + GXCopyDisp(GuestToHostPtr(da), (GXBool)c); + // Present immediately so post-copy draws don't leak into this frame. Join at the DONE phase + // (not the cheaper SEALED phase GXDrawDone waits for) because ImGui's draw lists, owned by + // Aurora's render worker, replay during encode; aurora_end_frame would join here anyway. + aurora_wait_for_frame_worker(); + } ++g_gxFrameCount; VI_HLE_SetXfbReady(da); - // Present immediately so post-copy draws don't leak into this frame. Join at the DONE phase - // (not the cheaper SEALED phase GXDrawDone waits for) because ImGui's draw lists, owned by - // Aurora's render worker, replay during encode; aurora_end_frame would join here anyway. - aurora_wait_for_frame_worker(); settings_overlay::Draw(); // Seal, pace to the VI retrace boundary (Aurora renders the sealed frame // during the wait), and pre-warm the next frame. @@ -135,7 +152,9 @@ PPC_NATIVE_OVERRIDE_VOID(8016fc38, GX__CopyDisp_8016fc38, (uint32_t da, uint32_t extern "C" void GX__CopyTex_8016fd74(uint32_t da, uint32_t c) { + // Begin the frame before going inline so a deferred begin reaches the worker first. EnsureAuroraFrameActive(); + GX_SYNC_INLINE(); // Match GX FIFO ordering: texture copies observe all prior draws. GXDrawDone(); const uint16_t rawSrcLeft = g_texCopyState.srcLeft; diff --git a/runtime/src/hle/gx/gx_dl.cpp b/runtime/src/hle/gx/gx_dl.cpp index 1d52eaf..19dbcf3 100644 --- a/runtime/src/hle/gx/gx_dl.cpp +++ b/runtime/src/hle/gx/gx_dl.cpp @@ -89,39 +89,42 @@ static inline uint32_t ReadModulatedLytColor(uint32_t colorAddr, int index, uint static_cast(a); } -static inline void AppendLytTexCoords(uint8_t* packet, uint32_t& pos, uint32_t texCoordAddr, - int texCoordCount, uint32_t cornerOffset) { - if (texCoordCount <= 0) { - return; - } - for (int i = 0; i < texCoordCount; ++i) { - const uint32_t coordAddr = texCoordAddr + static_cast(i * 32) + cornerOffset; - BigEndian::AppendFloat32(packet, pos, Memory::ReadFloat32(coordAddr)); - BigEndian::AppendFloat32(packet, pos, Memory::ReadFloat32(coordAddr + 4)); +// An nw4r::lyt quad as read on the game thread: texcoords are 8 sets of 4 corners x (s, t). +struct LytQuad { + float x0 = 0, y0 = 0, x1 = 0, y1 = 0; + int texCoordCount = 0; + std::array texCoords{}; + bool hasColors = false; + std::array colors{}; +}; + +static inline void AppendLytTexCoords(uint8_t* packet, uint32_t& pos, const LytQuad& quad, + uint32_t cornerOffset) { + for (int i = 0; i < quad.texCoordCount; ++i) { + const size_t at = static_cast(i) * 8u + cornerOffset / 4u; + BigEndian::AppendFloat32(packet, pos, quad.texCoords[at]); + BigEndian::AppendFloat32(packet, pos, quad.texCoords[at + 1]); } } -static inline void AppendLytQuadVertex(uint8_t* packet, uint32_t& pos, float x, float y, - uint32_t texCoordAddr, int texCoordCount, - uint32_t cornerOffset, const uint32_t* colors, int colorIndex) { +static inline void AppendLytQuadVertex(uint8_t* packet, uint32_t& pos, float x, float y, const LytQuad& quad, + uint32_t cornerOffset, int colorIndex) { BigEndian::AppendFloat32(packet, pos, x); BigEndian::AppendFloat32(packet, pos, y); - if (colors) { - BigEndian::Append32(packet, pos, colors[colorIndex]); + if (quad.hasColors) { + BigEndian::Append32(packet, pos, quad.colors[colorIndex]); } - AppendLytTexCoords(packet, pos, texCoordAddr, texCoordCount, cornerOffset); + AppendLytTexCoords(packet, pos, quad, cornerOffset); } // The four corners of an nw4r::lyt quad, in the order GX_QUADS expects, with // each corner's texcoord byte offset and colour index. The winding order is // load-bearing and both emit paths below share this one copy of it. -static inline void AppendLytQuadVertices(uint8_t* packet, uint32_t& pos, float x0, float y0, - float x1, float y1, uint32_t texCoordAddr, - int texCoordCount, const uint32_t* colors) { - AppendLytQuadVertex(packet, pos, x0, y0, texCoordAddr, texCoordCount, 0, colors, 0); - AppendLytQuadVertex(packet, pos, x1, y0, texCoordAddr, texCoordCount, 8, colors, 1); - AppendLytQuadVertex(packet, pos, x1, y1, texCoordAddr, texCoordCount, 24, colors, 3); - AppendLytQuadVertex(packet, pos, x0, y1, texCoordAddr, texCoordCount, 16, colors, 2); +static inline void AppendLytQuadVertices(uint8_t* packet, uint32_t& pos, const LytQuad& quad) { + AppendLytQuadVertex(packet, pos, quad.x0, quad.y0, quad, 0, 0); + AppendLytQuadVertex(packet, pos, quad.x1, quad.y0, quad, 8, 1); + AppendLytQuadVertex(packet, pos, quad.x1, quad.y1, quad, 24, 3); + AppendLytQuadVertex(packet, pos, quad.x0, quad.y1, quad, 16, 2); } static bool CanSubmitLytDrawDirect(int texCoordCount, const uint32_t* colors) { @@ -175,9 +178,8 @@ static bool CanSubmitLytDrawDirect(int texCoordCount, const uint32_t* colors) { return true; } -static bool SubmitLytDrawDirect(float x0, float y0, float x1, float y1, int texCoordCount, - uint32_t texCoordAddr, const uint32_t* colors) { - if (!CanSubmitLytDrawDirect(texCoordCount, colors)) { +static bool SubmitLytDrawDirect(const LytQuad& quad) { + if (!CanSubmitLytDrawDirect(quad.texCoordCount, quad.hasColors ? quad.colors.data() : nullptr)) { return false; } @@ -193,7 +195,7 @@ static bool SubmitLytDrawDirect(float x0, float y0, float x1, float y1, int texC std::array vertices{}; uint32_t pos = 0; - AppendLytQuadVertices(vertices.data(), pos, x0, y0, x1, y1, texCoordAddr, texCoordCount, colors); + AppendLytQuadVertices(vertices.data(), pos, quad); if (!aurora::gx::fifo::submit_raw_draw(GX_QUADS, GX_VTXFMT0, vertices.data(), 4, pos)) { return false; @@ -204,23 +206,8 @@ static bool SubmitLytDrawDirect(float x0, float y0, float x1, float y1, int texC return true; } -static inline void EmitLytDrawQuad(uint32_t posAddr, uint32_t sizeAddr, int texCoordCount, - uint32_t texCoordAddr, const uint32_t* colors) { - const float x0 = Memory::ReadFloat32(posAddr); - const float y0 = Memory::ReadFloat32(posAddr + 4); - // The casts pin the arithmetic to PPC single precision regardless of the - // host's FLT_EVAL_METHOD. - const float x1 = static_cast(x0 + Memory::ReadFloat32(sizeAddr)); - const float y1 = static_cast(y0 - Memory::ReadFloat32(sizeAddr + 4)); - - if (SubmitLytDrawDirect(x0, y0, x1, y1, texCoordCount, texCoordAddr, colors)) { - return; - } - - // GX has exactly 8 texture coordinates, so nw4r::lyt cannot ask for more. - // The fixed packet buffer below is sized for that maximum; bail rather than - // overrun it if the guest ever hands us something else. - if (texCoordCount > 8) { +static void EmitLytQuad(const LytQuad& quad) { + if (SubmitLytDrawDirect(quad)) { return; } @@ -228,10 +215,39 @@ static inline void EmitLytDrawQuad(uint32_t posAddr, uint32_t sizeAddr, int texC uint32_t pos = 0; packet[pos++] = GX_DRAW_QUADS_CMD | GX_VTXFMT0; BigEndian::Append16(packet.data(), pos, 4); - AppendLytQuadVertices(packet.data(), pos, x0, y0, x1, y1, texCoordAddr, texCoordCount, colors); + AppendLytQuadVertices(packet.data(), pos, quad); SubmitLytDrawPacket(packet.data(), pos); } +static inline void EmitLytDrawQuad(uint32_t posAddr, uint32_t sizeAddr, int texCoordCount, + uint32_t texCoordAddr, const uint32_t* colors) { + // GX has exactly 8 texture coordinates, so nw4r::lyt cannot ask for more; the packet buffer + // is sized for that maximum. + if (texCoordCount > 8) { + return; + } + LytQuad quad; + quad.x0 = Memory::ReadFloat32(posAddr); + quad.y0 = Memory::ReadFloat32(posAddr + 4); + // The casts pin the arithmetic to PPC single precision regardless of the + // host's FLT_EVAL_METHOD. + quad.x1 = static_cast(quad.x0 + Memory::ReadFloat32(sizeAddr)); + quad.y1 = static_cast(quad.y0 - Memory::ReadFloat32(sizeAddr + 4)); + quad.texCoordCount = std::max(texCoordCount, 0); + for (int i = 0; i < quad.texCoordCount; ++i) { + for (uint32_t f = 0; f < 8; ++f) { + quad.texCoords[static_cast(i) * 8u + f] = + Memory::ReadFloat32(texCoordAddr + static_cast(i * 32) + f * 4u); + } + } + if (colors != nullptr) { + quad.hasColors = true; + std::copy(colors, colors + 4, quad.colors.begin()); + } + EnsureAuroraFrameActive(); + GxWorker::Run([quad] { EmitLytQuad(quad); }); +} + static uint32_t SubmitDLVertex(const uint8_t* ptr, GXVtxFmt vtxfmt, const GXAttrType* sourceVtxDesc) { uint32_t offset = 0; for (int attr = 0; attr < 26; ++attr) { @@ -1284,6 +1300,12 @@ extern "C" void GxNotifyDisplayListMemoryWrite(uint32_t addr, uint32_t size) { extern "C" void GX__CallDisplayList_80172f64(uint32_t listAddr, uint32_t nbytes) { if (nbytes == 0 || listAddr == 0) return; + // List bytes and the arrays it indexes are GPU-side reads, so they are read when the worker runs. + if (GxWorker::ShouldDefer()) { + EnsureAuroraFrameActive(); + GxWorker::Submit([=] { GX__CallDisplayList_80172f64(listAddr, nbytes); }); + return; + } try { const uint8_t* list = static_cast(GuestToHostPtr(listAddr, nbytes)); if (!list) return; diff --git a/runtime/src/hle/gx/gx_egg.cpp b/runtime/src/hle/gx/gx_egg.cpp index 8a9fd75..4512ebe 100644 --- a/runtime/src/hle/gx/gx_egg.cpp +++ b/runtime/src/hle/gx/gx_egg.cpp @@ -81,8 +81,10 @@ extern "C" void EGG__LightTexture__SetupTevFinish_HLE_8022e2bc(CpuContext* ctx) if (remainder > 0) { const GXColor black{0, 0, 0, 255}; while (remainder < stageCount) { - GXSetTevColor(static_cast(remainder + 1), black); - GXSetTevKColor(static_cast(remainder), black); + GxWorker::Run([remainder, black] { + GXSetTevColor(static_cast(remainder + 1), black); + GXSetTevKColor(static_cast(remainder), black); + }); ++remainder; } diff --git a/runtime/src/hle/gx/gx_fifo.cpp b/runtime/src/hle/gx/gx_fifo.cpp index a77e96e..08282d7 100644 --- a/runtime/src/hle/gx/gx_fifo.cpp +++ b/runtime/src/hle/gx/gx_fifo.cpp @@ -775,11 +775,25 @@ static bool WriteDisplayListBurst(const uint8_t* data, uint32_t sizeBytes) { return true; } +static void FifoWriteBurstImpl(const uint8_t* data, uint32_t sizeBytes); + extern "C" void GX_HLE_FIFO_WriteBurst(const uint8_t* data, uint32_t sizeBytes) { if (data == nullptr || sizeBytes == 0) { return; } + if (GxWorker::ShouldDefer()) { + EnsureAuroraFrameForFifo(); + GxWorker::FifoBurst(data, sizeBytes); + return; + } + FifoWriteBurstImpl(data, sizeBytes); +} +void GxWorkerReplayFifoWrite(uint32_t value, uint32_t sizeBytes) { HleFifoWrite(value, sizeBytes); } + +void GxWorkerReplayFifoBurst(const uint8_t* data, uint32_t sizeBytes) { FifoWriteBurstImpl(data, sizeBytes); } + +static void FifoWriteBurstImpl(const uint8_t* data, uint32_t sizeBytes) { if (IsDisplayListActive() && WriteDisplayListBurst(data, sizeBytes)) { return; } diff --git a/runtime/src/hle/gx/gx_frame.cpp b/runtime/src/hle/gx/gx_frame.cpp index 6df3885..960cfe7 100644 --- a/runtime/src/hle/gx/gx_frame.cpp +++ b/runtime/src/hle/gx/gx_frame.cpp @@ -87,8 +87,51 @@ void BeginNextAuroraFrameWithRetry(std::chrono::milliseconds timeout) { ++s_beginExpiredLogCount; } +void EnsureAuroraFrameForFifo() { + // The game thread can't see where draws start in recorded FIFO bytes, so any write may begin the + // frame. A failed begin is only retried once the next frame is presented. + static int s_attemptedFrame = -1; + if (g_auroraFrameActive.load(std::memory_order_acquire) || s_attemptedFrame == g_gxFrameCount) { + return; + } + s_attemptedFrame = g_gxFrameCount; + UpdateAuroraAndProcessEvents(); + if (BeginAuroraFrame()) { + g_auroraFrameActive.store(true, std::memory_order_release); + } +} + void EnsureAuroraFrameActive() { + // The game thread begins frames before it records work for the GX worker. + if (GxWorker::OnWorker()) { + return; + } if (!g_auroraFrameActive.load(std::memory_order_acquire)) { BeginNextAuroraFrameWithRetry(); } } + +namespace { +// GX worker only. +bool g_workerFrameBegun = false; +} + +void GxWorkerBeginFrame() { + // gfx::begin_frame can briefly fail to map a staging buffer; retry like the inline path does. + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(100); + while (!(g_workerFrameBegun = aurora_begin_frame_gx()) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + if (!g_workerFrameBegun) { + RT_LOGF(RT_TAG_GX, "GX worker could not begin an aurora frame\n"); + } +} + +void GxWorkerEndFrame(AuroraImguiFrame* imgui) { + if (g_workerFrameBegun) { + aurora_end_frame_gx(imgui); + } else { + aurora_release_imgui_frame(imgui); + } + g_workerFrameBegun = false; +} diff --git a/runtime/src/hle/gx/gx_indirect.cpp b/runtime/src/hle/gx/gx_indirect.cpp index 7855f30..f8bb76c 100644 --- a/runtime/src/hle/gx/gx_indirect.cpp +++ b/runtime/src/hle/gx/gx_indirect.cpp @@ -5,22 +5,25 @@ // Indirect Texture Stages // ============================================================================ -extern "C" void GX__SetNumIndStages_80171b38(uint32_t n) { GXSetNumIndStages((u8)n); } +extern "C" void GX__SetNumIndStages_80171b38(uint32_t n) { + GX_DEFER_CALL(GX__SetNumIndStages_80171b38, n); GXSetNumIndStages((u8)n); } PPC_NATIVE_OVERRIDE_VOID(80171b38, GX__SetNumIndStages_80171b38, (uint32_t n), (n)); extern "C" void GX__SetIndTexOrder_80171a6c(uint32_t s, uint32_t c, uint32_t m) { + GX_DEFER_CALL(GX__SetIndTexOrder_80171a6c, s, c, m); GXSetIndTexOrder((GXIndTexStageID)s, (GXTexCoordID)(c==0xFFu?0:c), (GXTexMapID)(m==0xFFu?0:m)); } PPC_NATIVE_OVERRIDE_VOID(80171a6c, GX__SetIndTexOrder_80171a6c, (uint32_t s, uint32_t c, uint32_t m), (s, c, m)); extern "C" void GX__SetIndTexCoordScale_80171968(uint32_t s, uint32_t ss, uint32_t ts) { + GX_DEFER_CALL(GX__SetIndTexCoordScale_80171968, s, ss, ts); GXSetIndTexCoordScale((GXIndTexStageID)s, (GXIndTexScale)ss, (GXIndTexScale)ts); } PPC_NATIVE_OVERRIDE_VOID(80171968, GX__SetIndTexCoordScale_80171968, (uint32_t s, uint32_t ss, uint32_t ts), (s, ss, ts)); extern "C" void GX__SetIndTexMtx_80171814(uint32_t id, uint32_t ma, uint32_t se) { float m[6]; for(int i=0; i<6; ++i) m[i]=Memory::ReadFloat32(ma+i*4); - GXSetIndTexMtx((GXIndTexMtxID)id, m, (s8)se); + GxWorker::Run([id, se, mtx = std::to_array(m)]() mutable { GXSetIndTexMtx((GXIndTexMtxID)id, mtx.data(), (s8)se); }); } PPC_NATIVE_OVERRIDE_VOID(80171814, GX__SetIndTexMtx_80171814, (uint32_t id, uint32_t ma, uint32_t se), (id, ma, se)); @@ -28,16 +31,19 @@ PPC_NATIVE_OVERRIDE_VOID(80171814, GX__SetIndTexMtx_80171814, (uint32_t id, uint // TEV Indirect Texture Control // ============================================================================ -extern "C" void GX__SetTevDirect_80171b58(uint32_t s) { GXSetTevDirect((GXTevStageID)s); } +extern "C" void GX__SetTevDirect_80171b58(uint32_t s) { + GX_DEFER_CALL(GX__SetTevDirect_80171b58, s); GXSetTevDirect((GXTevStageID)s); } PPC_NATIVE_OVERRIDE_VOID(80171b58, GX__SetTevDirect_80171b58, (uint32_t s), (s)); extern "C" void GX__SetTevIndWarp_80171ba0(uint32_t ts, uint32_t is, uint32_t so, uint32_t rm, uint32_t ms) { + GX_DEFER_CALL(GX__SetTevIndWarp_80171ba0, ts, is, so, rm, ms); GXSetTevIndWarp((GXTevStageID)std::min(ts, 15u), (GXIndTexStageID)std::min(is, 3u), (GXBool)so, (GXBool)rm, (GXIndTexMtxID)ms); } PPC_NATIVE_OVERRIDE_VOID(80171ba0, GX__SetTevIndWarp_80171ba0, (uint32_t ts, uint32_t is, uint32_t so, uint32_t rm, uint32_t ms), (ts, is, so, rm, ms)); extern "C" void GX__SetTevIndirect_801717ac(uint32_t ts, uint32_t is, uint32_t f, uint32_t bs, uint32_t ms, uint32_t ws, uint32_t wt, uint32_t ap, uint32_t il, uint32_t as) { + GX_DEFER_CALL(GX__SetTevIndirect_801717ac, ts, is, f, bs, ms, ws, wt, ap, il, as); GXSetTevIndirect((GXTevStageID)std::min(ts,15u), (GXIndTexStageID)std::min(is,3u), (GXIndTexFormat)f, (GXIndTexBiasSel)bs, (GXIndTexMtxID)ms, (GXIndTexWrap)ws, (GXIndTexWrap)wt, (GXBool)ap, (GXBool)il, (GXIndTexAlphaSel)as); diff --git a/runtime/src/hle/gx/gx_init.cpp b/runtime/src/hle/gx/gx_init.cpp index d1cbaf4..4ab16fc 100644 --- a/runtime/src/hle/gx/gx_init.cpp +++ b/runtime/src/hle/gx/gx_init.cpp @@ -24,6 +24,7 @@ extern "C" GXFifoObj* GXInit(void* base, u32 size); */ extern "C" uint32_t GX__Init_8016b850(uint32_t fifoBase, uint32_t fifoSize) { + GX_SYNC_INLINE(); constexpr uint32_t kFifoObjAddr = 0x80343740u; constexpr uint32_t kGXDataAddr = 0x803437C0u; constexpr uint32_t kGXDataSize = 0x600u; @@ -91,16 +92,16 @@ PPC_NATIVE_OVERRIDE(8016b850, GX__Init_8016b850, uint32_t, (uint32_t fifoBase, u extern "C" void GX__InitFifoBase_8016c7c8(uint32_t fa, uint32_t ba, uint32_t s) { GXInitFifoBase((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj)), GuestToHostPtr(ba, s), s); } -extern "C" void GX__SetCPUFifo_8016c94c(uint32_t fa) { GXSetCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); } +extern "C" void GX__SetCPUFifo_8016c94c(uint32_t fa) { GX_SYNC_INLINE(); GXSetCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); } PPC_NATIVE_OVERRIDE_VOID(8016c94c, GX__SetCPUFifo_8016c94c, (uint32_t fa), (fa)); -extern "C" void GX__SetGPFifo_8016cb2c(uint32_t fa) { GXSetGPFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); } +extern "C" void GX__SetGPFifo_8016cb2c(uint32_t fa) { GX_SYNC_INLINE(); GXSetGPFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); } PPC_NATIVE_OVERRIDE_VOID(8016cb2c, GX__SetGPFifo_8016cb2c, (uint32_t fa), (fa)); -extern "C" void __GX__SaveFifo_8016cdbc(uint32_t fa) { GXSaveCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); } +extern "C" void __GX__SaveFifo_8016cdbc(uint32_t fa) { GX_SYNC_INLINE(); GXSaveCPUFifo((GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj))); } PPC_NATIVE_OVERRIDE_VOID(8016cdbc, __GX__SaveFifo_8016cdbc, (uint32_t fa), (fa)); -extern "C" void GX__GetCPUFifo_8016cf10(uint32_t fa) { auto* d=(GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj)); auto* s=GXGetCPUFifo(); if(d&&s) std::memcpy(d,s,sizeof(GXFifoObj)); } +extern "C" void GX__GetCPUFifo_8016cf10(uint32_t fa) { GX_SYNC_INLINE(); auto* d=(GXFifoObj*)GuestToHostPtr(fa, sizeof(GXFifoObj)); auto* s=GXGetCPUFifo(); if(d&&s) std::memcpy(d,s,sizeof(GXFifoObj)); } PPC_NATIVE_OVERRIDE_VOID(8016cf10, GX__GetCPUFifo_8016cf10, (uint32_t fa), (fa)); extern "C" void __GX__FifoInit_8016d180() @@ -137,6 +138,7 @@ extern "C" void __GX__FifoInit_8016d180() extern "C" void __GX__PEInit_8016ee14() { + GX_SYNC_INLINE(); constexpr uint32_t kPeTokenInterruptId = 0x12u; constexpr uint32_t kPeFinishInterruptId = 0x13u; constexpr uint32_t kPeTokenInterruptMask = 0x1000u; @@ -165,7 +167,16 @@ PPC_NATIVE_OVERRIDE_VOID(8016ee14, __GX__PEInit_8016ee14, (), ()); // Display List Recording // ============================================================================ +namespace { +// Display-list recording runs inline: the guest list buffer and its size are needed synchronously. +bool g_dlInline = false; +} + extern "C" void GX__BeginDisplayList_80172e00(uint32_t la, uint32_t s) { + if (!g_dlInline) { + GxWorker::BeginInline(); + g_dlInline = true; + } try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (!gd) return; @@ -188,6 +199,13 @@ extern "C" void GX__BeginDisplayList_80172e00(uint32_t la, uint32_t s) { } PPC_NATIVE_OVERRIDE_VOID(80172e00, GX__BeginDisplayList_80172e00, (uint32_t la, uint32_t s), (la, s)); +static void EndDisplayListInline() { + if (g_dlInline) { + g_dlInline = false; + GxWorker::EndInline(); + } +} + extern "C" uint32_t GX__EndDisplayList_80172eb4() { try { GXFlush(); @@ -214,12 +232,14 @@ extern "C" uint32_t GX__EndDisplayList_80172eb4() { // "inactive" whenever the GXData pointer was null, so recording must // stop here even on the gd == 0 path. EndDisplayListRecording(); + EndDisplayListInline(); return wrapped == 0 ? Memory::Read32(kDlCountAddr) : 0; } catch (...) { // Leaving the shadow active would silently swallow every subsequent // FIFO write into a dead list. EndDisplayListRecording(); + EndDisplayListInline(); return 0; } } @@ -229,5 +249,8 @@ PPC_NATIVE_OVERRIDE(80172EB4, GX__EndDisplayList_80172eb4, uint32_t, (), ()); // Flush // ============================================================================ -extern "C" void GX__Flush_8016e654() { GXFlush(); } +extern "C" void GX__Flush_8016e654() { + GX_DEFER_CALL0(GX__Flush_8016e654); + GXFlush(); +} PPC_NATIVE_OVERRIDE_VOID(8016e654, GX__Flush_8016e654, (), ()); diff --git a/runtime/src/hle/gx/gx_internal.h b/runtime/src/hle/gx/gx_internal.h index 8656dba..149f776 100644 --- a/runtime/src/hle/gx/gx_internal.h +++ b/runtime/src/hle/gx/gx_internal.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -300,6 +301,9 @@ void EndDisplayListRecording(); void WriteDisplayListData(uint32_t val, uint32_t sizeBytes); void BeginNextAuroraFrameWithRetry(std::chrono::milliseconds timeout = std::chrono::milliseconds(100)); void EnsureAuroraFrameActive(); +void EnsureAuroraFrameForFifo(); +// Array base/stride as last set through GXSetArray on the game thread. +bool ApiVtxArray(uint32_t attr, uint32_t& base, uint32_t& stride); GXColor DecodeGxColor(uint32_t colorWord); void WriteGuestFloat(uint32_t addr, float value, const char* label = nullptr); @@ -342,6 +346,17 @@ bool ValidateTlutData(uint32_t objAddr, const TlutObjMeta& meta); TexObjMeta& GetTexObjMeta(uint32_t addr); TexObjMeta ExtractTexObjMetaFromGuest(uint32_t addr); bool TryGetOrExtractTexObjMeta(uint32_t addr, TexObjMeta& outMeta); + +// Guest GXTexObj bytes captured on the game thread; while a scope is active on a thread, texobj +// decoding for that address reads the captured bytes instead of live guest memory. +using TexObjBytes = std::array; +TexObjBytes CaptureTexObjBytes(uint32_t addr); +struct TexObjSnapshotScope { + TexObjSnapshotScope(uint32_t addr, const TexObjBytes& bytes) noexcept; + ~TexObjSnapshotScope(); + TexObjSnapshotScope(const TexObjSnapshotScope&) = delete; + TexObjSnapshotScope& operator=(const TexObjSnapshotScope&) = delete; +}; TlutObjMeta& GetTlutObjMeta(uint32_t addr); GXTexObj* GetHostTexObj(uint32_t addr); GXTexObj* TryGetHostTexObj(uint32_t addr); diff --git a/runtime/src/hle/gx/gx_lighting.cpp b/runtime/src/hle/gx/gx_lighting.cpp index 1b1d392..3b186cf 100644 --- a/runtime/src/hle/gx/gx_lighting.cpp +++ b/runtime/src/hle/gx/gx_lighting.cpp @@ -66,7 +66,7 @@ extern "C" void GX__LoadLightObjImm_80170320(uint32_t la, uint32_t lid) { if (!ValidateGuestLightObj(la, "GXLoadLightObjImm")) return; GXLightObj host{}; InitializeHostLightFromGuest(host, la); - GXLoadLightObjImm(&host, static_cast(lid)); + GxWorker::Run([host, lid]() mutable { GXLoadLightObjImm(&host, static_cast(lid)); }); } PPC_NATIVE_OVERRIDE_VOID(80170320, GX__LoadLightObjImm_80170320, (uint32_t la, uint32_t lid), (la, lid)); @@ -77,21 +77,23 @@ PPC_NATIVE_OVERRIDE_VOID(80170320, GX__LoadLightObjImm_80170320, (uint32_t la, u extern "C" void GX__SetChanAmbColor_8017039c(uint32_t c, uint32_t cp) { EnsureAuroraFrameActive(); GXColor color = DecodeGxColor(Memory::Read32(cp)); - GXSetChanAmbColor((GXChannelID)c, color); + GxWorker::Run([c, color] { GXSetChanAmbColor((GXChannelID)c, color); }); } PPC_NATIVE_OVERRIDE_VOID(8017039c, GX__SetChanAmbColor_8017039c, (uint32_t c, uint32_t cp), (c, cp)); extern "C" void GX__SetChanMatColor_80170474(uint32_t c, uint32_t cp) { EnsureAuroraFrameActive(); GXColor color = DecodeGxColor(Memory::Read32(cp)); - GXSetChanMatColor((GXChannelID)c, color); + GxWorker::Run([c, color] { GXSetChanMatColor((GXChannelID)c, color); }); } PPC_NATIVE_OVERRIDE_VOID(80170474, GX__SetChanMatColor_80170474, (uint32_t c, uint32_t cp), (c, cp)); -extern "C" void GX__SetNumChans_8017054c(uint32_t n) { GXSetNumChans((u8)n); } +extern "C" void GX__SetNumChans_8017054c(uint32_t n) { + GX_DEFER_CALL(GX__SetNumChans_8017054c, n); GXSetNumChans((u8)n); } PPC_NATIVE_OVERRIDE_VOID(8017054c, GX__SetNumChans_8017054c, (uint32_t n), (n)); extern "C" void GX__SetChanCtrl_80170570(uint32_t ch, uint32_t en, uint32_t as, uint32_t ms, uint32_t lm, uint32_t df, uint32_t af) { + GX_DEFER_CALL(GX__SetChanCtrl_80170570, ch, en, as, ms, lm, df, af); GXSetChanCtrl((GXChannelID)ch, en!=0, (GXColorSrc)as, (GXColorSrc)ms, lm, (GXDiffuseFn)df, (GXAttnFn)af); } PPC_NATIVE_OVERRIDE_VOID(80170570, GX__SetChanCtrl_80170570, (uint32_t ch, uint32_t en, uint32_t as, uint32_t ms, uint32_t lm, uint32_t df, uint32_t af), (ch, en, as, ms, lm, df, af)); diff --git a/runtime/src/hle/gx/gx_objects.cpp b/runtime/src/hle/gx/gx_objects.cpp index ce4f368..645658d 100644 --- a/runtime/src/hle/gx/gx_objects.cpp +++ b/runtime/src/hle/gx/gx_objects.cpp @@ -110,17 +110,65 @@ bool g_tlutObjBoundsDirty = true; // word3 data address (<<5), word4 user data, word5 texFmt, word6 tlut, byte 0x1F flags. Exact // bit positions are decoded inline below. +namespace { +thread_local uint32_t t_texObjSnapshotAddr = 0; +thread_local const uint8_t* t_texObjSnapshotBytes = nullptr; + +const uint8_t* TexObjSnapshotFor(uint32_t addr) noexcept { + return (t_texObjSnapshotBytes != nullptr && addr == t_texObjSnapshotAddr) ? t_texObjSnapshotBytes : nullptr; +} + +uint32_t ReadTexObj32(uint32_t addr, uint32_t offset) { + if (const uint8_t* bytes = TexObjSnapshotFor(addr)) return BigEndian::Read32(bytes + offset); + return Memory::Read32(addr + offset); +} + +uint8_t ReadTexObj8(uint32_t addr, uint32_t offset) { + if (const uint8_t* bytes = TexObjSnapshotFor(addr)) return bytes[offset]; + return Memory::Read8(addr + offset); +} + +uint64_t ReadTexObj64(uint32_t addr, uint32_t offset) { + if (const uint8_t* bytes = TexObjSnapshotFor(addr)) { + return (static_cast(BigEndian::Read32(bytes + offset)) << 32) | BigEndian::Read32(bytes + offset + 4); + } + return Memory::Read64(addr + offset); +} +} // namespace + +TexObjBytes CaptureTexObjBytes(uint32_t addr) { + TexObjBytes bytes{}; + for (uint32_t offset = 0; offset < bytes.size(); offset += 4) { + const uint32_t word = Memory::Read32(addr + offset); + bytes[offset + 0] = static_cast(word >> 24); + bytes[offset + 1] = static_cast(word >> 16); + bytes[offset + 2] = static_cast(word >> 8); + bytes[offset + 3] = static_cast(word); + } + return bytes; +} + +TexObjSnapshotScope::TexObjSnapshotScope(uint32_t addr, const TexObjBytes& bytes) noexcept { + t_texObjSnapshotAddr = addr; + t_texObjSnapshotBytes = bytes.data(); +} + +TexObjSnapshotScope::~TexObjSnapshotScope() { + t_texObjSnapshotAddr = 0; + t_texObjSnapshotBytes = nullptr; +} + TexObjMeta ExtractTexObjMetaFromGuest(uint32_t addr) { TexObjMeta meta{}; try { - uint32_t word0 = Memory::Read32(addr + 0x00); - uint32_t word1 = Memory::Read32(addr + 0x04); - uint32_t word2 = Memory::Read32(addr + 0x08); - uint32_t word3 = Memory::Read32(addr + 0x0C); - uint32_t word4 = Memory::Read32(addr + 0x10); - uint32_t word5 = Memory::Read32(addr + 0x14); - uint32_t word6 = Memory::Read32(addr + 0x18); - uint8_t flags = Memory::Read8(addr + 0x1F); + uint32_t word0 = ReadTexObj32(addr, 0x00); + uint32_t word1 = ReadTexObj32(addr, 0x04); + uint32_t word2 = ReadTexObj32(addr, 0x08); + uint32_t word3 = ReadTexObj32(addr, 0x0C); + uint32_t word4 = ReadTexObj32(addr, 0x10); + uint32_t word5 = ReadTexObj32(addr, 0x14); + uint32_t word6 = ReadTexObj32(addr, 0x18); + uint8_t flags = ReadTexObj8(addr, 0x1F); meta.wrapS = word0 & 0x3; meta.wrapT = (word0 >> 2) & 0x3; @@ -449,10 +497,10 @@ TexObjMeta MergeGuestTexObjMeta(const TexObjMeta& cached, const TexObjMeta& gues // caller must treat the shadow as absent) when the address is unreadable. bool ReadTexObjShadow(uint32_t addr, uint64_t (&out)[4]) noexcept { try { - out[0] = Memory::Read64(addr + 0x00); - out[1] = Memory::Read64(addr + 0x08); - out[2] = Memory::Read64(addr + 0x10); - out[3] = Memory::Read64(addr + 0x18); + out[0] = ReadTexObj64(addr, 0x00); + out[1] = ReadTexObj64(addr, 0x08); + out[2] = ReadTexObj64(addr, 0x10); + out[3] = ReadTexObj64(addr, 0x18); } catch (...) { return false; } @@ -642,8 +690,11 @@ extern "C" void GxNotifyGuestRamDmaWrite(uint32_t addr, uint32_t size) { if (size == 0) { return; } - MarkTexObjsDirtyForRange(addr, size); - MarkTlutObjsDirtyForRange(addr, size); + // Ordered with recorded GX work: retiring EFB copy textures emits aurora commands. + GxWorker::Run([addr, size] { + MarkTexObjsDirtyForRange(addr, size); + MarkTlutObjsDirtyForRange(addr, size); + }); GxNotifyDisplayListMemoryWrite(addr, size); } diff --git a/runtime/src/hle/gx/gx_pixel.cpp b/runtime/src/hle/gx/gx_pixel.cpp index 8866ab1..68182a5 100644 --- a/runtime/src/hle/gx/gx_pixel.cpp +++ b/runtime/src/hle/gx/gx_pixel.cpp @@ -6,17 +6,20 @@ // ============================================================================ extern "C" void GX__SetBlendMode_8017277c(uint32_t t, uint32_t s, uint32_t d, uint32_t op) { + GX_DEFER_CALL(GX__SetBlendMode_8017277c, t, s, d, op); GXSetBlendMode(static_cast(t), static_cast(s), static_cast(d), static_cast(op)); } PPC_NATIVE_OVERRIDE_VOID(8017277c, GX__SetBlendMode_8017277c, (uint32_t t, uint32_t s, uint32_t d, uint32_t op), (t, s, d, op)); extern "C" void GX__SetColorUpdate_801727cc(uint32_t en) { + GX_DEFER_CALL(GX__SetColorUpdate_801727cc, en); GXSetColorUpdate(static_cast(en)); } PPC_NATIVE_OVERRIDE_VOID(801727cc, GX__SetColorUpdate_801727cc, (uint32_t en), (en)); extern "C" void GX__SetAlphaUpdate_801727f8(uint32_t en) { + GX_DEFER_CALL(GX__SetAlphaUpdate_801727f8, en); GXSetAlphaUpdate(static_cast(en)); } PPC_NATIVE_OVERRIDE_VOID(801727f8, GX__SetAlphaUpdate_801727f8, (uint32_t en), (en)); @@ -26,11 +29,13 @@ PPC_NATIVE_OVERRIDE_VOID(801727f8, GX__SetAlphaUpdate_801727f8, (uint32_t en), ( // ============================================================================ extern "C" void GX__SetZMode_80172824(uint32_t ce, uint32_t f, uint32_t ue) { + GX_DEFER_CALL(GX__SetZMode_80172824, ce, f, ue); GXSetZMode(static_cast(ce), static_cast(f), static_cast(ue)); } PPC_NATIVE_OVERRIDE_VOID(80172824, GX__SetZMode_80172824, (uint32_t ce, uint32_t f, uint32_t ue), (ce, f, ue)); extern "C" void GX__SetZCompLoc_80172858(uint32_t bt) { + GX_DEFER_CALL(GX__SetZCompLoc_80172858, bt); GXSetZCompLoc(static_cast(bt)); } PPC_NATIVE_OVERRIDE_VOID(80172858, GX__SetZCompLoc_80172858, (uint32_t bt), (bt)); @@ -40,16 +45,19 @@ PPC_NATIVE_OVERRIDE_VOID(80172858, GX__SetZCompLoc_80172858, (uint32_t bt), (bt) // ============================================================================ extern "C" void GX__SetPixelFmt_80172888(uint32_t pf, uint32_t zf) { + GX_DEFER_CALL(GX__SetPixelFmt_80172888, pf, zf); GXSetPixelFmt(static_cast(pf), static_cast(zf)); } PPC_NATIVE_OVERRIDE_VOID(80172888, GX__SetPixelFmt_80172888, (uint32_t pf, uint32_t zf), (pf, zf)); extern "C" void GX__SetDither_80172930(uint32_t d) { + GX_DEFER_CALL(GX__SetDither_80172930, d); GXSetDither(static_cast(d)); } PPC_NATIVE_OVERRIDE_VOID(80172930, GX__SetDither_80172930, (uint32_t d), (d)); extern "C" void GX__SetDstAlpha_8017295c(uint32_t en, uint32_t a) { + GX_DEFER_CALL(GX__SetDstAlpha_8017295c, en, a); GXSetDstAlpha(static_cast(en), static_cast(a)); } PPC_NATIVE_OVERRIDE_VOID(8017295c, GX__SetDstAlpha_8017295c, (uint32_t en, uint32_t a), (en, a)); @@ -59,17 +67,20 @@ PPC_NATIVE_OVERRIDE_VOID(8017295c, GX__SetDstAlpha_8017295c, (uint32_t en, uint3 // ============================================================================ extern "C" void GX__SetFog_801722cc(uint32_t t, float sz, float ez, float nz, float fz, uint32_t cp) { - GXSetFog((GXFogType)t, sz, ez, nz, fz, DecodeGxColor(Memory::Read32(cp))); + const GXColor color = DecodeGxColor(Memory::Read32(cp)); + GxWorker::Run([=] { GXSetFog((GXFogType)t, sz, ez, nz, fz, color); }); } PPC_NATIVE_OVERRIDE_VOID(801722cc, GX__SetFog_801722cc, (uint32_t t, float sz, float ez, float nz, float fz, uint32_t cp), (t, sz, ez, nz, fz, cp)); extern "C" void GX__SetAlphaCompare_80172088(uint32_t c0, uint32_t r0, uint32_t op, uint32_t c1, uint32_t r1) { + GX_DEFER_CALL(GX__SetAlphaCompare_80172088, c0, r0, op, c1, r1); g_alphaCompareValid = true; GXSetAlphaCompare((GXCompare)c0, (u8)r0, (GXAlphaOp)op, (GXCompare)c1, (u8)r1); } PPC_NATIVE_OVERRIDE_VOID(80172088, GX__SetAlphaCompare_80172088, (uint32_t c0, uint32_t r0, uint32_t op, uint32_t c1, uint32_t r1), (c0, r0, op, c1, r1)); -extern "C" void GX__SetZTexture_801720c0(uint32_t op, uint32_t f, uint32_t b) { GXSetZTexture((GXZTexOp)op, (GXTexFmt)f, b); } +extern "C" void GX__SetZTexture_801720c0(uint32_t op, uint32_t f, uint32_t b) { + GX_DEFER_CALL(GX__SetZTexture_801720c0, op, f, b); GXSetZTexture((GXZTexOp)op, (GXTexFmt)f, b); } PPC_NATIVE_OVERRIDE_VOID(801720c0, GX__SetZTexture_801720c0, (uint32_t op, uint32_t f, uint32_t b), (op, f, b)); // ============================================================================ @@ -77,12 +88,15 @@ PPC_NATIVE_OVERRIDE_VOID(801720c0, GX__SetZTexture_801720c0, (uint32_t op, uint3 // ============================================================================ extern "C" void GX__SetCullMode_8016f3b8(uint32_t m) { + GX_DEFER_CALL(GX__SetCullMode_8016f3b8, m); GXSetCullMode(static_cast(m)); } PPC_NATIVE_OVERRIDE_VOID(8016f3b8, GX__SetCullMode_8016f3b8, (uint32_t m), (m)); -extern "C" void GX__SetCoPlanar_8016f3e0(uint32_t en) { GXSetCoPlanar((GXBool)en); } +extern "C" void GX__SetCoPlanar_8016f3e0(uint32_t en) { + GX_DEFER_CALL(GX__SetCoPlanar_8016f3e0, en); GXSetCoPlanar((GXBool)en); } PPC_NATIVE_OVERRIDE_VOID(8016f3e0, GX__SetCoPlanar_8016f3e0, (uint32_t en), (en)); -extern "C" void GX__SetClipMode_8017351c(uint32_t m) { GXSetClipMode((GXClipMode)m); } +extern "C" void GX__SetClipMode_8017351c(uint32_t m) { + GX_DEFER_CALL(GX__SetClipMode_8017351c, m); GXSetClipMode((GXClipMode)m); } PPC_NATIVE_OVERRIDE_VOID(8017351c, GX__SetClipMode_8017351c, (uint32_t m), (m)); diff --git a/runtime/src/hle/gx/gx_stubs.cpp b/runtime/src/hle/gx/gx_stubs.cpp index bba6585..edbafdc 100644 --- a/runtime/src/hle/gx/gx_stubs.cpp +++ b/runtime/src/hle/gx/gx_stubs.cpp @@ -7,14 +7,23 @@ extern "C" void __GXSetSUTexRegs(); // FIFO Write Helpers // ============================================================================ -extern "C" void GX_HLE_FIFO_WriteFloat(float val) { - u32 raw; std::memcpy(&raw, &val, 4); - try { HleFifoWrite(raw, 4); } catch (...) { RT_LOGF(RT_TAG_GX, "FIFO write float failed\n"); } +static void FifoWrite(u32 val, uint32_t sizeBytes) { + if (GxWorker::ShouldDefer()) { + EnsureAuroraFrameForFifo(); + GxWorker::FifoWrite(val, sizeBytes); + return; + } + HleFifoWrite(val, sizeBytes); } -extern "C" void GX_HLE_FIFO_Write32(uint32_t val) { HleFifoWrite(val, 4); } -extern "C" void GX_HLE_FIFO_Write16(uint16_t val) { HleFifoWrite(static_cast(val), 2); } -extern "C" void GX_HLE_FIFO_Write8(uint8_t val) { HleFifoWrite(static_cast(val), 1); } +extern "C" void GX_HLE_FIFO_WriteFloat(float val) { + u32 raw; std::memcpy(&raw, &val, 4); + try { FifoWrite(raw, 4); } catch (...) { RT_LOGF(RT_TAG_GX, "FIFO write float failed\n"); } +} + +extern "C" void GX_HLE_FIFO_Write32(uint32_t val) { FifoWrite(val, 4); } +extern "C" void GX_HLE_FIFO_Write16(uint16_t val) { FifoWrite(static_cast(val), 2); } +extern "C" void GX_HLE_FIFO_Write8(uint8_t val) { FifoWrite(static_cast(val), 1); } extern "C" void GX__SetDrawSync_8016ed08(uint32_t token) { (void)token; @@ -37,6 +46,7 @@ extern "C" void GX__FinishInterruptHandler_8016ed94() { PPC_NATIVE_OVERRIDE_VOID(8016ed94, GX__FinishInterruptHandler_8016ed94, (), ()); extern "C" void GX__DrawDone_8016eab0() { + GX_SYNC_INLINE(); try { Memory::Write8(kGxDrawDoneFlagAddr, 0); } catch (...) {} GXDrawDone(); GX__FinishInterruptHandler_8016ed94(); } @@ -44,7 +54,7 @@ PPC_NATIVE_OVERRIDE_VOID(8016eab0, GX__DrawDone_8016eab0, (), ()); extern "C" void GX__PixModeSync_8016eb70() { try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {} - GXPixModeSync(); + GxWorker::Run([] { GXPixModeSync(); }); } PPC_NATIVE_OVERRIDE_VOID(8016eb70, GX__PixModeSync_8016eb70, (), ()); @@ -60,7 +70,7 @@ PPC_NATIVE_OVERRIDE_VOID(8016b720, __GX__InitRevisionBits_8016b720, (), ()); // ============================================================================ extern "C" void __GX__SetSUTexRegs_801712f0() { - __GXSetSUTexRegs(); + GxWorker::Run([] { __GXSetSUTexRegs(); }); try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {} } PPC_NATIVE_OVERRIDE_VOID(801712f0, __GX__SetSUTexRegs_801712f0, (), ()); @@ -82,7 +92,7 @@ PPC_NATIVE_OVERRIDE_VOID(80171c28, __GX__FlushTextureState_80171c28, (), ()); // ============================================================================ extern "C" void GX__SetDispCopyFrame2Field_8016f5f8(uint32_t f) { - GXSetDispCopyFrame2Field(f); + GxWorker::Run([f] { GXSetDispCopyFrame2Field(f); }); try { const uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) { @@ -94,7 +104,7 @@ extern "C" void GX__SetDispCopyFrame2Field_8016f5f8(uint32_t f) { PPC_NATIVE_OVERRIDE_VOID(8016f5f8, GX__SetDispCopyFrame2Field_8016f5f8, (uint32_t f), (f)); extern "C" void GX__SetCopyClamp_8016f618(uint32_t c) { - GXSetCopyClamp(static_cast(c)); + GxWorker::Run([c] { GXSetCopyClamp(static_cast(c)); }); try { const uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) { @@ -107,7 +117,7 @@ extern "C" void GX__SetCopyClamp_8016f618(uint32_t c) { PPC_NATIVE_OVERRIDE_VOID(8016f618, GX__SetCopyClamp_8016f618, (uint32_t c), (c)); extern "C" void GX__ClearBoundingBox_8016fecc() { - GXClearBoundingBox(); + GxWorker::Run([] { GXClearBoundingBox(); }); try { const uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); diff --git a/runtime/src/hle/gx/gx_tev.cpp b/runtime/src/hle/gx/gx_tev.cpp index 54c01a9..57ab64b 100644 --- a/runtime/src/hle/gx/gx_tev.cpp +++ b/runtime/src/hle/gx/gx_tev.cpp @@ -25,6 +25,7 @@ inline bool TevSwapOk(uint32_t id) { return GxTevIdOk(id, GX_MAX_TEVSWAP, "TEV s // ============================================================================ extern "C" void GX__SetNumTevStages_801722a8(uint32_t n) { + GX_DEFER_CALL(GX__SetNumTevStages_801722a8, n); // GXSetNumTevStages takes a count, not an index, so the inclusive bound is // GX_MAX_TEVSTAGE itself. if (n > GX_MAX_TEVSTAGE) { @@ -36,12 +37,14 @@ extern "C" void GX__SetNumTevStages_801722a8(uint32_t n) { PPC_NATIVE_OVERRIDE_VOID(801722a8, GX__SetNumTevStages_801722a8, (uint32_t n), (n)); extern "C" void GX__SetTevOp_80171c4c(uint32_t s, uint32_t m) { + GX_DEFER_CALL(GX__SetTevOp_80171c4c, s, m); if (!TevStageOk(s)) return; GXSetTevOp((GXTevStageID)s, (GXTevMode)m); } PPC_NATIVE_OVERRIDE_VOID(80171c4c, GX__SetTevOp_80171c4c, (uint32_t s, uint32_t m), (s, m)); extern "C" void GX__SetTevOrder_8017214c(uint32_t s, uint32_t c, uint32_t m, uint32_t col) { + GX_DEFER_CALL(GX__SetTevOrder_8017214c, s, c, m, col); if (!TevStageOk(s)) return; GXSetTevOrder((GXTevStageID)s, (GXTexCoordID)c, (GXTexMapID)m, (GXChannelID)col); } @@ -52,12 +55,14 @@ PPC_NATIVE_OVERRIDE_VOID(8017214c, GX__SetTevOrder_8017214c, (uint32_t s, uint32 // ============================================================================ extern "C" void GX__SetTevColorIn_80171ce0(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d) { + GX_DEFER_CALL(GX__SetTevColorIn_80171ce0, s, a, b, c, d); if (!TevStageOk(s)) return; GXSetTevColorIn((GXTevStageID)s, (GXTevColorArg)a, (GXTevColorArg)b, (GXTevColorArg)c, (GXTevColorArg)d); } PPC_NATIVE_OVERRIDE_VOID(80171ce0, GX__SetTevColorIn_80171ce0, (uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d), (s, a, b, c, d)); extern "C" void GX__SetTevAlphaIn_80171d20(uint32_t s, uint32_t a, uint32_t b, uint32_t c, uint32_t d) { + GX_DEFER_CALL(GX__SetTevAlphaIn_80171d20, s, a, b, c, d); if (!TevStageOk(s)) return; GXSetTevAlphaIn((GXTevStageID)s, (GXTevAlphaArg)a, (GXTevAlphaArg)b, (GXTevAlphaArg)c, (GXTevAlphaArg)d); } @@ -68,12 +73,14 @@ PPC_NATIVE_OVERRIDE_VOID(80171d20, GX__SetTevAlphaIn_80171d20, (uint32_t s, uint // ============================================================================ extern "C" void GX__SetTevColorOp_80171d60(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_) { + GX_DEFER_CALL(GX__SetTevColorOp_80171d60, s, op, b, sc, cl, or_); if (!TevStageOk(s) || !TevRegOk(or_)) return; GXSetTevColorOp((GXTevStageID)s, (GXTevOp)op, (GXTevBias)b, (GXTevScale)sc, (GXBool)cl, (GXTevRegID)or_); } PPC_NATIVE_OVERRIDE_VOID(80171d60, GX__SetTevColorOp_80171d60, (uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_), (s, op, b, sc, cl, or_)); extern "C" void GX__SetTevAlphaOp_80171db8(uint32_t s, uint32_t op, uint32_t b, uint32_t sc, uint32_t cl, uint32_t or_) { + GX_DEFER_CALL(GX__SetTevAlphaOp_80171db8, s, op, b, sc, cl, or_); if (!TevStageOk(s) || !TevRegOk(or_)) return; GXSetTevAlphaOp((GXTevStageID)s, (GXTevOp)op, (GXTevBias)b, (GXTevScale)sc, (GXBool)cl, (GXTevRegID)or_); } @@ -86,7 +93,7 @@ PPC_NATIVE_OVERRIDE_VOID(80171db8, GX__SetTevAlphaOp_80171db8, (uint32_t s, uint extern "C" void GX__SetTevColor_80171e10(uint32_t id, uint32_t cp) { if (!TevRegOk(id)) return; const uint8_t* p=Memory::GetPointer(cp, 4); GXColor c; c.r=p[0]; c.g=p[1]; c.b=p[2]; c.a=p[3]; - GXSetTevColor((GXTevRegID)id, c); + GxWorker::Run([id, c] { GXSetTevColor((GXTevRegID)id, c); }); } PPC_NATIVE_OVERRIDE_VOID(80171e10, GX__SetTevColor_80171e10, (uint32_t id, uint32_t cp), (id, cp)); @@ -94,21 +101,23 @@ extern "C" void GX__SetTevColorS10_80171e70(uint32_t id, uint32_t cp) { if (!TevRegOk(id)) return; const uint8_t* p=Memory::GetPointer(cp, 8); GXColorS10 c; c.r=(p[0]<<8)|p[1]; c.g=(p[2]<<8)|p[3]; c.b=(p[4]<<8)|p[5]; c.a=(p[6]<<8)|p[7]; - GXSetTevColorS10((GXTevRegID)id, c); + GxWorker::Run([id, c] { GXSetTevColorS10((GXTevRegID)id, c); }); } PPC_NATIVE_OVERRIDE_VOID(80171e70, GX__SetTevColorS10_80171e70, (uint32_t id, uint32_t cp), (id, cp)); extern "C" void GX__SetTevKColor_80171ed4(uint32_t id, uint32_t cp) { if (!TevKColorOk(id)) return; const uint8_t* p=Memory::GetPointer(cp, 4); GXColor c; c.r=p[0]; c.g=p[1]; c.b=p[2]; c.a=p[3]; - GXSetTevKColor((GXTevKColorID)id, c); + GxWorker::Run([id, c] { GXSetTevKColor((GXTevKColorID)id, c); }); } PPC_NATIVE_OVERRIDE_VOID(80171ed4, GX__SetTevKColor_80171ed4, (uint32_t id, uint32_t cp), (id, cp)); -extern "C" void GX__SetTevKColorSel_80171f30(uint32_t s, uint32_t sel) { if (!TevStageOk(s)) return; GXSetTevKColorSel((GXTevStageID)s, (GXTevKColorSel)sel); } +extern "C" void GX__SetTevKColorSel_80171f30(uint32_t s, uint32_t sel) { + GX_DEFER_CALL(GX__SetTevKColorSel_80171f30, s, sel); if (!TevStageOk(s)) return; GXSetTevKColorSel((GXTevStageID)s, (GXTevKColorSel)sel); } PPC_NATIVE_OVERRIDE_VOID(80171f30, GX__SetTevKColorSel_80171f30, (uint32_t s, uint32_t sel), (s, sel)); -extern "C" void GX__SetTevKAlphaSel_80171f80(uint32_t s, uint32_t sel) { if (!TevStageOk(s)) return; GXSetTevKAlphaSel((GXTevStageID)s, (GXTevKAlphaSel)sel); } +extern "C" void GX__SetTevKAlphaSel_80171f80(uint32_t s, uint32_t sel) { + GX_DEFER_CALL(GX__SetTevKAlphaSel_80171f80, s, sel); if (!TevStageOk(s)) return; GXSetTevKAlphaSel((GXTevStageID)s, (GXTevKAlphaSel)sel); } PPC_NATIVE_OVERRIDE_VOID(80171f80, GX__SetTevKAlphaSel_80171f80, (uint32_t s, uint32_t sel), (s, sel)); // ============================================================================ @@ -116,12 +125,14 @@ PPC_NATIVE_OVERRIDE_VOID(80171f80, GX__SetTevKAlphaSel_80171f80, (uint32_t s, ui // ============================================================================ extern "C" void GX__SetTevSwapModeTable_8017200c(uint32_t id, uint32_t r, uint32_t g, uint32_t b, uint32_t a) { + GX_DEFER_CALL(GX__SetTevSwapModeTable_8017200c, id, r, g, b, a); if (!TevSwapOk(id)) return; GXSetTevSwapModeTable((GXTevSwapSel)id, (GXTevColorChan)r, (GXTevColorChan)g, (GXTevColorChan)b, (GXTevColorChan)a); } PPC_NATIVE_OVERRIDE_VOID(8017200c, GX__SetTevSwapModeTable_8017200c, (uint32_t id, uint32_t r, uint32_t g, uint32_t b, uint32_t a), (id, r, g, b, a)); extern "C" void GX__SetTevSwapMode_80171fd0(uint32_t s, uint32_t rs, uint32_t ts) { + GX_DEFER_CALL(GX__SetTevSwapMode_80171fd0, s, rs, ts); if (!TevStageOk(s) || !TevSwapOk(rs) || !TevSwapOk(ts)) return; GXSetTevSwapMode((GXTevStageID)s, (GXTevSwapSel)rs, (GXTevSwapSel)ts); } diff --git a/runtime/src/hle/gx/gx_texture.cpp b/runtime/src/hle/gx/gx_texture.cpp index b5ebedd..c3c6746 100644 --- a/runtime/src/hle/gx/gx_texture.cpp +++ b/runtime/src/hle/gx/gx_texture.cpp @@ -242,6 +242,31 @@ static uint32_t ComputeMaxMipLevel(uint16_t width, uint16_t height) noexcept { return levels; } +// GXInitTexObj* writes the guest struct synchronously; the host half runs on the GX worker with the +// struct bytes from before that write, which is what an inline call's shadow capture sees. +template +static void InitTexObjSplit(uint32_t oa, Hle&& hle, Guest&& guest) { + if (!GxWorker::ShouldDefer()) { + hle(); + guest(); + return; + } + std::optional before; + try { + before = CaptureTexObjBytes(oa); + } catch (...) { + } + guest(); + GxWorker::Submit([oa, before, hle = std::forward(hle)] { + if (before) { + TexObjSnapshotScope snapshot(oa, *before); + hle(); + } else { + hle(); + } + }); +} + extern "C" void GX__InitTexObj_801707f8(uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m) { if (w == 0 || h == 0) { auto* cpu = TryGetCpuContext(); @@ -250,12 +275,15 @@ extern "C" void GX__InitTexObj_801707f8(uint32_t oa, uint32_t da, uint32_t w, ui oa, da, w, h, f, ws, wt, m, cpu ? cpu->pc : 0u, cpu ? cpu->lr : 0u); } const uint32_t canonicalDataAddr = CanonicalizeGxMainRamAddress(da); - std::lock_guard guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa); - meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.userData=0; meta.needsUpload=true; - GXInitTexObj(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXTexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m); MarkHostTexObjConstructed(oa); - // Also write to guest memory so reads work - WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, false, 0); + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa); + meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.userData=0; meta.needsUpload=true; + GXInitTexObj(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXTexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m); MarkHostTexObjConstructed(oa); + }, [=] { + WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, false, 0); + }); } + PPC_NATIVE_OVERRIDE_VOID(801707f8, GX__InitTexObj_801707f8, (uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m), (oa, da, w, h, f, ws, wt, m)); extern "C" void GX__InitTexObj_switch_80170938(uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m) { GX__InitTexObj_801707f8(oa, da, w, h, f, ws, wt, m); } @@ -291,132 +319,151 @@ extern "C" void GX__InitTexObjCI_80170a04(uint32_t oa, uint32_t da, uint32_t w, oa, da, w, h, f, ws, wt, m, tl, cpu ? cpu->pc : 0u, cpu ? cpu->lr : 0u); } const uint32_t canonicalDataAddr = CanonicalizeGxMainRamAddress(da); - std::lock_guard guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa); - meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.tlut=tl; meta.userData=0; meta.needsUpload=true; - GXInitTexObjCI(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXCITexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m, tl); MarkHostTexObjConstructed(oa); - // Also write to guest memory so reads work - WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, true, tl); - // GXInitTexObjCI clears bit1 in the flags byte; keep guest memory consistent. - try { - uint8_t flags = Memory::Read8(oa + 0x1F); - Memory::Write8(oa + 0x1F, static_cast(flags & 0xFD)); - } catch (...) { - } + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); GXTexObj* obj = CreateHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa); + meta.dataAddr=canonicalDataAddr; meta.width=(u16)w; meta.height=(u16)h; meta.format=f; meta.wrapS=ws; meta.wrapT=wt; meta.mipmap=(m!=0); meta.tlut=tl; meta.userData=0; meta.needsUpload=true; + GXInitTexObjCI(obj, GuestToHostPtr(da), (u16)w, (u16)h, (GXCITexFmt)f, (GXTexWrapMode)ws, (GXTexWrapMode)wt, (GXBool)m, tl); MarkHostTexObjConstructed(oa); + }, [=] { + WriteGuestTexObj(oa, canonicalDataAddr, (u16)w, (u16)h, f, ws, wt, m != 0, true, tl); + // GXInitTexObjCI clears bit1 in the flags byte; keep guest memory consistent. + try { + uint8_t flags = Memory::Read8(oa + 0x1F); + Memory::Write8(oa + 0x1F, static_cast(flags & 0xFD)); + } catch (...) { + } + }); } + PPC_NATIVE_OVERRIDE_VOID(80170a04, GX__InitTexObjCI_80170a04, (uint32_t oa, uint32_t da, uint32_t w, uint32_t h, uint32_t f, uint32_t ws, uint32_t wt, uint32_t m, uint32_t tl), (oa, da, w, h, f, ws, wt, m, tl)); extern "C" void GX__InitTexObjLOD_80170a4c(uint32_t oa, uint32_t mif, uint32_t maf, float mil, float mal, float lb, uint32_t bc, uint32_t el, uint32_t ma) { - std::lock_guard guard(g_texObjMutex); GXTexObj* obj = GetHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa); - float fmal = std::isfinite(mal) && mal >= 0.f ? mal : 0.f, fmil = std::isfinite(mil) && mil >= 0.f ? std::min(mil, fmal) : 0.f; - meta.minFilter=mif; meta.magFilter=maf; meta.minLod=fmil; meta.maxLod=fmal; meta.lodBias=lb; meta.biasClamp=(bc!=0); meta.edgeLod=(el!=0); meta.maxAniso=ma; - GXInitTexObjLOD(obj, (GXTexFilter)mif, (GXTexFilter)maf, fmil, fmal, lb, (GXBool)bc, (GXBool)el, (GXAnisotropy)ma); - // Also write LOD info to guest memory - WriteGuestTexObjLOD(oa, mif, maf, fmil, fmal, lb, bc != 0, el != 0, ma); + const float fmal = std::isfinite(mal) && mal >= 0.f ? mal : 0.f, fmil = std::isfinite(mil) && mil >= 0.f ? std::min(mil, fmal) : 0.f; + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); GXTexObj* obj = GetHostTexObj(oa); TexObjMeta& meta = GetTexObjMeta(oa); + meta.minFilter=mif; meta.magFilter=maf; meta.minLod=fmil; meta.maxLod=fmal; meta.lodBias=lb; meta.biasClamp=(bc!=0); meta.edgeLod=(el!=0); meta.maxAniso=ma; + GXInitTexObjLOD(obj, (GXTexFilter)mif, (GXTexFilter)maf, fmil, fmal, lb, (GXBool)bc, (GXBool)el, (GXAnisotropy)ma); + }, [=] { + WriteGuestTexObjLOD(oa, mif, maf, fmil, fmal, lb, bc != 0, el != 0, ma); + }); } + PPC_NATIVE_OVERRIDE_VOID(80170a4c, GX__InitTexObjLOD_80170a4c, (uint32_t oa, uint32_t mif, uint32_t maf, float mil, float mal, float lb, uint32_t bc, uint32_t el, uint32_t ma), (oa, mif, maf, mil, mal, lb, bc, el, ma)); extern "C" void GX__InitTexObjWrapMode_80170b50(uint32_t oa, uint32_t ws, uint32_t wt) { - std::lock_guard guard(g_texObjMutex); - GXTexObj* obj = GetHostTexObj(oa); - TexObjMeta& meta = GetTexObjMeta(oa); - meta.wrapS = ws; - meta.wrapT = wt; - try { - uint32_t word0 = Memory::Read32(oa + 0x00); - word0 = (word0 & ~0xFu) | (ws & 0x3u) | ((wt & 0x3u) << 2); - Memory::Write32(oa + 0x00, word0); - } catch (...) { - } - GXInitTexObjWrapMode(obj, (GXTexWrapMode)ws, (GXTexWrapMode)wt); + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); + GXTexObj* obj = GetHostTexObj(oa); + TexObjMeta& meta = GetTexObjMeta(oa); + meta.wrapS = ws; + meta.wrapT = wt; + GXInitTexObjWrapMode(obj, (GXTexWrapMode)ws, (GXTexWrapMode)wt); + }, [=] { + try { + uint32_t word0 = Memory::Read32(oa + 0x00); + word0 = (word0 & ~0xFu) | (ws & 0x3u) | ((wt & 0x3u) << 2); + Memory::Write32(oa + 0x00, word0); + } catch (...) { + } + }); } + PPC_NATIVE_OVERRIDE_VOID(80170b50, GX__InitTexObjWrapMode_80170b50, (uint32_t oa, uint32_t ws, uint32_t wt), (oa, ws, wt)); extern "C" void GX__InitTexObjTlut_80170b64(uint32_t oa, uint32_t tl) { - std::lock_guard guard(g_texObjMutex); - GXTexObj* obj = GetHostTexObj(oa); - TexObjMeta& meta = GetTexObjMeta(oa); - meta.tlut = tl; - GXInitTexObjTlut(obj, tl); - // Keep guest GXTexObj coherent for later GXLoadTexObj from memory. - try { - Memory::Write32(oa + 0x18, tl); - } catch (...) { - } + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); + GXTexObj* obj = GetHostTexObj(oa); + TexObjMeta& meta = GetTexObjMeta(oa); + meta.tlut = tl; + GXInitTexObjTlut(obj, tl); + }, [=] { + // Keep guest GXTexObj coherent for later GXLoadTexObj from memory. + try { + Memory::Write32(oa + 0x18, tl); + } catch (...) { + } + }); } + PPC_NATIVE_OVERRIDE_VOID(80170b64, GX__InitTexObjTlut_80170b64, (uint32_t oa, uint32_t tl), (oa, tl)); // GXInitTexObjFilter - sets mag/min filter modes extern "C" void GX__InitTexObjFilter_80170b6c(uint32_t oa, uint32_t minFilter, uint32_t magFilter) { - std::lock_guard guard(g_texObjMutex); - TexObjMeta& meta = GetTexObjMeta(oa); - meta.minFilter = minFilter; - meta.magFilter = magFilter; - - try { - uint32_t word0 = Memory::Read32(oa + 0x00); - // bit 4: magFilter - set if magFilter == GX_LINEAR (1), clear otherwise - // Original code used cntlzw to check: cntlzw(magFilter-1) >> 1 & 0x10 - // This sets bit 4 when magFilter-1 is 0 (i.e., magFilter == 1) - uint32_t magBit = (magFilter == 1) ? 0x10 : 0; - word0 = (word0 & 0xFFFFFFEF) | magBit; - // bits [7:5]: minFilter from LUT - uint32_t minBits = (kGxToHwMinFilter[minFilter & 7] & 7) << 5; - word0 = (word0 & 0xFFFFFF0F) | magBit | minBits; - Memory::Write32(oa + 0x00, word0); - } catch (...) {} - - // Update host texture object if it exists - if (GXTexObj* obj = TryGetHostTexObj(oa)) { - ApplyHostTexObjLod(obj, meta, (GXTexFilter)minFilter, (GXTexFilter)magFilter, meta.lodBias, - (GXAnisotropy)meta.maxAniso); - } + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); + TexObjMeta& meta = GetTexObjMeta(oa); + meta.minFilter = minFilter; + meta.magFilter = magFilter; + // Update host texture object if it exists + if (GXTexObj* obj = TryGetHostTexObj(oa)) { + ApplyHostTexObjLod(obj, meta, (GXTexFilter)minFilter, (GXTexFilter)magFilter, meta.lodBias, + (GXAnisotropy)meta.maxAniso); + } + }, [=] { + try { + uint32_t word0 = Memory::Read32(oa + 0x00); + // bit 4: magFilter - set if magFilter == GX_LINEAR (1), clear otherwise + // Original code used cntlzw to check: cntlzw(magFilter-1) >> 1 & 0x10 + // This sets bit 4 when magFilter-1 is 0 (i.e., magFilter == 1) + uint32_t magBit = (magFilter == 1) ? 0x10 : 0; + word0 = (word0 & 0xFFFFFFEF) | magBit; + // bits [7:5]: minFilter from LUT + uint32_t minBits = (kGxToHwMinFilter[minFilter & 7] & 7) << 5; + word0 = (word0 & 0xFFFFFF0F) | magBit | minBits; + Memory::Write32(oa + 0x00, word0); + } catch (...) {} + }); } + PPC_NATIVE_OVERRIDE_VOID(80170b6c, GX__InitTexObjFilter_80170b6c, (uint32_t oa, uint32_t minFilter, uint32_t magFilter), (oa, minFilter, magFilter)); // GXInitTexObjLODBias - sets LOD bias value extern "C" void GX__InitTexObjLODBias_80170b94(uint32_t oa, float bias) { - std::lock_guard guard(g_texObjMutex); - TexObjMeta& meta = GetTexObjMeta(oa); - // Clamp bias to [-4.0, 3.99...] range as per SDK constexpr float kMinBias = -4.0f; constexpr float kMaxBias = 3.99f; // Actually ~3.990000009536743 float clampedBias = bias; if (clampedBias < kMinBias) clampedBias = kMinBias; if (clampedBias >= 4.0f) clampedBias = kMaxBias; - - meta.lodBias = clampedBias; - - // Update guest GXTexObj word0 with bias (scale by 32, store in bits [16:9]) - try { - uint32_t word0 = Memory::Read32(oa + 0x00); - int8_t biasScaled = static_cast(clampedBias * 32.0f); - word0 = (word0 & 0xFFFE01FF) | ((static_cast(static_cast(biasScaled)) & 0xFF) << 9); - Memory::Write32(oa + 0x00, word0); - } catch (...) {} - - // Update host texture object if it exists - if (GXTexObj* obj = TryGetHostTexObj(oa)) { - ApplyHostTexObjLod(obj, meta, (GXTexFilter)meta.minFilter, (GXTexFilter)meta.magFilter, - clampedBias, (GXAnisotropy)meta.maxAniso); - } + + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); + TexObjMeta& meta = GetTexObjMeta(oa); + meta.lodBias = clampedBias; + // Update host texture object if it exists + if (GXTexObj* obj = TryGetHostTexObj(oa)) { + ApplyHostTexObjLod(obj, meta, (GXTexFilter)meta.minFilter, (GXTexFilter)meta.magFilter, + clampedBias, (GXAnisotropy)meta.maxAniso); + } + }, [=] { + // Update guest GXTexObj word0 with bias (scale by 32, store in bits [16:9]) + try { + uint32_t word0 = Memory::Read32(oa + 0x00); + int8_t biasScaled = static_cast(clampedBias * 32.0f); + word0 = (word0 & 0xFFFE01FF) | ((static_cast(static_cast(biasScaled)) & 0xFF) << 9); + Memory::Write32(oa + 0x00, word0); + } catch (...) {} + }); } + PPC_NATIVE_OVERRIDE_VOID(80170b94, GX__InitTexObjLODBias_80170b94, (uint32_t oa, float bias), (oa, bias)); extern "C" void GX__InitTexObjUserData_80170be8(uint32_t oa, uint32_t userData) { - std::lock_guard guard(g_texObjMutex); - TexObjMeta& meta = GetTexObjMeta(oa); - meta.userData = userData; - - // The RVL SDK stores this opaque guest pointer verbatim at GXTexObj + 0x10. - WriteGuest32(oa + 0x10, userData, "GXInitTexObjUserData"); - - // Aurora keeps an expanded host-side object, so mirror the pointer when - // that representation has already been constructed. - if (GXTexObj* obj = TryGetHostTexObj(oa)) { - GXInitTexObjUserData(obj, GuestToHostPtr(userData)); - } + InitTexObjSplit(oa, [=] { + std::lock_guard guard(g_texObjMutex); + TexObjMeta& meta = GetTexObjMeta(oa); + meta.userData = userData; + // Aurora keeps an expanded host-side object, so mirror the pointer when + // that representation has already been constructed. + if (GXTexObj* obj = TryGetHostTexObj(oa)) { + GXInitTexObjUserData(obj, GuestToHostPtr(userData)); + } + }, [=] { + // The RVL SDK stores this opaque guest pointer verbatim at GXTexObj + 0x10. + WriteGuest32(oa + 0x10, userData, "GXInitTexObjUserData"); + }); } + PPC_NATIVE_OVERRIDE_VOID(80170be8, GX__InitTexObjUserData_80170be8, (uint32_t oa, uint32_t userData), (oa, userData)); @@ -446,7 +493,7 @@ static void BindUnloadableTexturePlaceholder(uint32_t tid) { GXLoadTexObj(&s_placeholder, (GXTexMapID)tid); } -extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) { +static void LoadTexObjImpl(uint32_t oa, uint32_t tid) { static uint32_t s_invalidMetaLogCount = 0; static uint32_t s_invalidTidLogCount = 0; static uint32_t s_invalidDimLogCount = 0; @@ -631,6 +678,27 @@ extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) { BindUnloadableTexturePlaceholder(tid); return; } +} + +extern "C" void GX__LoadTexObj_80170f2c(uint32_t oa, uint32_t tid) { + if (GxWorker::ShouldDefer()) { + // The CPU-side SDK reads the GXTexObj at call time; the texture data itself is read later. + std::optional bytes; + try { + bytes = CaptureTexObjBytes(oa); + } catch (...) { + } + GxWorker::Submit([oa, tid, bytes] { + if (bytes) { + TexObjSnapshotScope snapshot(oa, *bytes); + LoadTexObjImpl(oa, tid); + } else { + LoadTexObjImpl(oa, tid); + } + }); + } else { + LoadTexObjImpl(oa, tid); + } try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) { Memory::Write32(gd + 0x5FCu, Memory::Read32(gd + 0x5FCu) | 1u); Memory::Write16(gd + 2, 0); } } catch (...) {} } PPC_NATIVE_OVERRIDE_VOID(80170f2c, GX__LoadTexObj_80170f2c, (uint32_t oa, uint32_t tid), (oa, tid)); @@ -643,6 +711,7 @@ PPC_NATIVE_OVERRIDE_VOID(80170dc8, GX__LoadTexObjPreLoaded_80170dc8, (uint32_t o // ============================================================================ extern "C" void GX__GetTexObjAll_80170bf8(uint32_t oa, uint32_t dp, uint32_t wp, uint32_t hp, uint32_t fp, uint32_t wsp, uint32_t wtp, uint32_t mp) { + GX_SYNC_INLINE(); std::lock_guard guard(g_texObjMutex); TexObjMeta meta; if (!TryGetOrExtractTexObjMeta(oa, meta)) { @@ -657,6 +726,7 @@ extern "C" void GX__GetTexObjAll_80170bf8(uint32_t oa, uint32_t dp, uint32_t wp, PPC_NATIVE_OVERRIDE_VOID(80170bf8, GX__GetTexObjAll_80170bf8, (uint32_t oa, uint32_t dp, uint32_t wp, uint32_t hp, uint32_t fp, uint32_t wsp, uint32_t wtp, uint32_t mp), (oa, dp, wp, hp, fp, wsp, wtp, mp)); extern "C" void GX__GetTexObjLODAll_80170cbc(uint32_t oa, uint32_t mifp, uint32_t mafp, uint32_t milp, uint32_t malp, uint32_t lbp, uint32_t bcp, uint32_t elp, uint32_t map) { + GX_SYNC_INLINE(); std::lock_guard guard(g_texObjMutex); TexObjMeta meta; if (!TryGetOrExtractTexObjMeta(oa, meta)) { @@ -676,6 +746,7 @@ PPC_NATIVE_OVERRIDE_VOID(80170cbc, GX__GetTexObjLODAll_80170cbc, (uint32_t oa, u // ============================================================================ extern "C" void GX__InitTlutObj_80170f80(uint32_t oa, uint32_t da, uint32_t f, uint32_t e) { + GX_DEFER_CALL(GX__InitTlutObj_80170f80, oa, da, f, e); std::lock_guard guard(g_tlutObjMutex); GXTlutObj* obj = CreateHostTlutObj(oa); TlutObjMeta& meta = GetTlutObjMeta(oa); meta.dataAddr=CanonicalizeGxMainRamAddress(da); meta.format=f; meta.entries=(u16)e; meta.dirty=false; // Leave the object unconstructed on a bad descriptor. A later GXLoadTlut @@ -686,7 +757,12 @@ extern "C" void GX__InitTlutObj_80170f80(uint32_t oa, uint32_t da, uint32_t f, u } PPC_NATIVE_OVERRIDE_VOID(80170f80, GX__InitTlutObj_80170f80, (uint32_t oa, uint32_t da, uint32_t f, uint32_t e), (oa, da, f, e)); -extern "C" void GX__LoadTlut_80170fa8(uint32_t oa, uint32_t tl) { std::lock_guard guard(g_tlutObjMutex); if (tl >= kMaxTluts) { RT_LOGF(RT_TAG_GX, "GXLoadTlut: invalid TLUT index %u (oa=0x%08X)\n", tl, oa); return; } auto metaIt = g_TlutObjMeta.find(oa); if (metaIt != g_TlutObjMeta.end() && metaIt->second.dirty) { if (ValidateTlutData(oa, metaIt->second)) { GXTlutObj* rebuild = CreateHostTlutObj(oa); GXInitTlutObj(rebuild, GuestToHostPtr(metaIt->second.dataAddr), (GXTlutFmt)metaIt->second.format, metaIt->second.entries); MarkHostTlutObjConstructed(oa); } metaIt->second.dirty = false; } GXTlutObj* obj = GetHostTlutObj(oa); GXLoadTlut(obj, (GXTlut)tl); try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {} } +static void LoadTlutImpl(uint32_t oa, uint32_t tl) { std::lock_guard guard(g_tlutObjMutex); if (tl >= kMaxTluts) { RT_LOGF(RT_TAG_GX, "GXLoadTlut: invalid TLUT index %u (oa=0x%08X)\n", tl, oa); return; } auto metaIt = g_TlutObjMeta.find(oa); if (metaIt != g_TlutObjMeta.end() && metaIt->second.dirty) { if (ValidateTlutData(oa, metaIt->second)) { GXTlutObj* rebuild = CreateHostTlutObj(oa); GXInitTlutObj(rebuild, GuestToHostPtr(metaIt->second.dataAddr), (GXTlutFmt)metaIt->second.format, metaIt->second.entries); MarkHostTlutObjConstructed(oa); } metaIt->second.dirty = false; } GXTlutObj* obj = GetHostTlutObj(oa); GXLoadTlut(obj, (GXTlut)tl); } + +extern "C" void GX__LoadTlut_80170fa8(uint32_t oa, uint32_t tl) { + GxWorker::Run([oa, tl] { LoadTlutImpl(oa, tl); }); + try { uint32_t gd = Memory::Read32(kGXDataPtrAddr); if (gd) Memory::Write16(gd + 2, 0); } catch (...) {} +} PPC_NATIVE_OVERRIDE_VOID(80170fa8, GX__LoadTlut_80170fa8, (uint32_t oa, uint32_t tl), (oa, tl)); // ============================================================================ @@ -694,6 +770,7 @@ PPC_NATIVE_OVERRIDE_VOID(80170fa8, GX__LoadTlut_80170fa8, (uint32_t oa, uint32_t // ============================================================================ extern "C" void GX__InvalidateTexAll_80171110() { + GX_DEFER_CALL0(GX__InvalidateTexAll_80171110); // Real GX invalidates its internal texture cache here. Aurora forwards the // invalidate to the renderer, which keeps unchanged source uploads hot but // revalidates reused guest buffers before serving cached texture handles. @@ -701,8 +778,8 @@ extern "C" void GX__InvalidateTexAll_80171110() { } PPC_NATIVE_OVERRIDE_VOID(80171110, GX__InvalidateTexAll_80171110, (), ()); -extern "C" void GX__SetTexCoordScaleManually_80171180(uint32_t c, uint32_t en, uint32_t ss, uint32_t ts) { GXSetTexCoordScaleManually((GXTexCoordID)c, (GXBool)en, (u16)ss, (u16)ts); try{ uint32_t gd=Memory::Read32(kGXDataPtrAddr); if(gd){ Memory::Write32(gd+0x5E4u, (Memory::Read32(gd+0x5E4u)&~(1u< apply_logical_render_state), // so re-issuing the current viewport would be duplicate work. - GXSetScissor(l, t, w, h); + GxWorker::Run([l, t, w, h] { GXSetScissor(l, t, w, h); }); } PPC_NATIVE_OVERRIDE_VOID(80173430, GX__SetScissor_80173430, (uint32_t l, uint32_t t, uint32_t w, uint32_t h), (l, t, w, h)); @@ -115,7 +115,7 @@ PPC_NATIVE_OVERRIDE_VOID(80173430, GX__SetScissor_80173430, (uint32_t l, uint32_ extern "C" void GX__SetProjection_8017301c(uint32_t ma, uint32_t pt) { const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma, 64); float m[16]; SwapBeF32ArrayToHost(raw, m, 16); - GXSetProjection(m, (GXProjectionType)pt); + GxWorker::Run([mtx = std::to_array(m), pt]() mutable { GXSetProjection(mtx.data(), (GXProjectionType)pt); }); UpdateProjectionVectorFromMatrix(m, (GXProjectionType)pt); } PPC_NATIVE_OVERRIDE_VOID(8017301c, GX__SetProjection_8017301c, (uint32_t ma, uint32_t pt), (ma, pt)); @@ -126,7 +126,7 @@ extern "C" void GX__SetProjectionv_80173080(uint32_t pa) { GXProjectionType pt=(v[0]!=0.f)?GX_ORTHOGRAPHIC:GX_PERSPECTIVE; float m[16]={0.f}; m[0]=v[1]; m[5]=v[3]; m[10]=v[5]; m[11]=v[6]; if(pt==GX_PERSPECTIVE){ m[2]=v[2]; m[6]=v[4]; m[14]=-1.f; } else { m[3]=v[2]; m[7]=v[4]; m[15]=1.f; } - GXSetProjection(m, pt); + GxWorker::Run([mtx = std::to_array(m), pt]() mutable { GXSetProjection(mtx.data(), pt); }); UpdateProjectionVectorFromProjV(v); } PPC_NATIVE_OVERRIDE_VOID(80173080, GX__SetProjectionv_80173080, (uint32_t pa), (pa)); @@ -146,28 +146,51 @@ PPC_NATIVE_OVERRIDE_VOID(801730cc, GX__GetProjectionv_801730cc, (uint32_t pa), ( extern "C" void GX__LoadPosMtxImm_8017310c(uint32_t ma, uint32_t id) { const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma); float m[12]; SwapBeF32ArrayToHost(raw, m, 12); - GXLoadPosMtxImm((float(*)[4])m, id); + GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadPosMtxImm((float(*)[4])mtx.data(), id); }); } PPC_NATIVE_OVERRIDE_VOID(8017310c, GX__LoadPosMtxImm_8017310c, (uint32_t ma, uint32_t id), (ma, id)); +// The matrix is read at load time, as Dolphin does, since the worker can run well behind the game. +// Returns false only when the game thread doesn't know the array and the worker must resolve it. +static bool IndexedMtxArray(uint32_t attr, uint32_t& base, uint32_t& stride) { + if (GxWorker::ShouldDefer()) { + return ApiVtxArray(attr, base, stride); + } + base = g_hleGxState.vtxArray[attr].base; + stride = g_hleGxState.vtxArray[attr].stride; + return true; +} + extern "C" void GX__LoadPosMtxIndx_8017315c(uint32_t mi, uint32_t id) { - const auto& arr=g_hleGxState.vtxArray[GX_POS_MTX_ARRAY]; - if(arr.base==0||arr.stride==0) return; - const uint32_t* raw=(const uint32_t*)GuestToHostPtr(arr.base+mi*arr.stride, 48); - if(raw){ float m[12]; SwapBeF32ArrayToHost(raw,m,12); GXLoadPosMtxImm((float(*)[4])m,id); } + uint32_t base = 0, stride = 0; + if (!IndexedMtxArray(GX_POS_MTX_ARRAY, base, stride)) { + GX_DEFER_CALL(GX__LoadPosMtxIndx_8017315c, mi, id); + return; + } + if (base == 0 || stride == 0) return; + const uint32_t* raw=(const uint32_t*)GuestToHostPtr(base+mi*stride, 48); + if(raw){ + float m[12]; SwapBeF32ArrayToHost(raw,m,12); + GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadPosMtxImm((float(*)[4])mtx.data(), id); }); + } } PPC_NATIVE_OVERRIDE_VOID(8017315c, GX__LoadPosMtxIndx_8017315c, (uint32_t mi, uint32_t id), (mi, id)); extern "C" void GX__LoadNrmMtxImm_80173188(uint32_t ma, uint32_t id) { const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma, 48); float m[12]; - SwapBeF32ArrayToHost(raw, m, 12); GXLoadNrmMtxImm((float(*)[4])m, id); + SwapBeF32ArrayToHost(raw, m, 12); + GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadNrmMtxImm((float(*)[4])mtx.data(), id); }); } PPC_NATIVE_OVERRIDE_VOID(80173188, GX__LoadNrmMtxImm_80173188, (uint32_t ma, uint32_t id), (ma, id)); extern "C" void GX__LoadNrmMtxIndx3x3_801731e0(uint32_t mi, uint32_t id) { - const auto& arr=g_hleGxState.vtxArray[GX_NRM_MTX_ARRAY]; - if(arr.base==0||arr.stride==0) return; - const uint32_t* raw=(const uint32_t*)GuestToHostPtr(arr.base+mi*arr.stride, 36); + uint32_t base = 0, stride = 0; + if (!IndexedMtxArray(GX_NRM_MTX_ARRAY, base, stride)) { + GX_DEFER_CALL(GX__LoadNrmMtxIndx3x3_801731e0, mi, id); + return; + } + if (base == 0 || stride == 0) return; + const uint32_t* raw=(const uint32_t*)GuestToHostPtr(base+mi*stride, 36); if(raw){ float src[9]{}; float m[12]{}; @@ -177,7 +200,7 @@ extern "C" void GX__LoadNrmMtxIndx3x3_801731e0(uint32_t mi, uint32_t id) { m[row * 4 + col] = src[row * 3 + col]; } } - GXLoadNrmMtxImm((float(*)[4])m, id); + GxWorker::Run([mtx = std::to_array(m), id]() mutable { GXLoadNrmMtxImm((float(*)[4])mtx.data(), id); }); } } PPC_NATIVE_OVERRIDE_VOID(801731e0, GX__LoadNrmMtxIndx3x3_801731e0, (uint32_t mi, uint32_t id), (mi, id)); @@ -185,9 +208,13 @@ PPC_NATIVE_OVERRIDE_VOID(801731e0, GX__LoadNrmMtxIndx3x3_801731e0, (uint32_t mi, extern "C" void GX__LoadTexMtxImm_80173234(uint32_t ma, uint32_t id, uint32_t t) { size_t c=(t==(uint32_t)GX_MTX3x4)?12:8; const uint32_t* raw=(const uint32_t*)GuestToHostPtr(ma,c*4); float l[12]={}; - SwapBeF32ArrayToHost(raw,l,c); GXLoadTexMtxImm(l, id, (GXTexMtxType)t); + SwapBeF32ArrayToHost(raw,l,c); + GxWorker::Run([mtx = std::to_array(l), id, t]() mutable { GXLoadTexMtxImm(mtx.data(), id, (GXTexMtxType)t); }); } PPC_NATIVE_OVERRIDE_VOID(80173234, GX__LoadTexMtxImm_80173234, (uint32_t ma, uint32_t id, uint32_t t), (ma, id, t)); -extern "C" void GX__SetCurrentMtx_80173214(uint32_t id) { GXSetCurrentMtx(id); } +extern "C" void GX__SetCurrentMtx_80173214(uint32_t id) { + GX_DEFER_CALL(GX__SetCurrentMtx_80173214, id); + GXSetCurrentMtx(id); +} PPC_NATIVE_OVERRIDE_VOID(80173214, GX__SetCurrentMtx_80173214, (uint32_t id), (id)); diff --git a/runtime/src/hle/gx/gx_vertex.cpp b/runtime/src/hle/gx/gx_vertex.cpp index 68cbf92..6f87268 100644 --- a/runtime/src/hle/gx/gx_vertex.cpp +++ b/runtime/src/hle/gx/gx_vertex.cpp @@ -13,6 +13,9 @@ uint32_t CanonicalVtxAttr(uint32_t attr) { return attr == GX_VA_NBT ? GX_VA_NRM : attr; } +// Array bases as last set through GXSetArray on the game thread. +std::array, 26> g_apiVtxArrays{}; + // There is deliberately no indexed-aurora path here. The immediate begin path // streams vertex data incrementally, and aurora's indexed-array upload must // precede the draw carrying the max-index bounds, which cannot be known without @@ -27,11 +30,19 @@ void ApplyAuroraVtxStateForBegin(GXVtxFmt fmt) { } } +bool ApiVtxArray(uint32_t attr, uint32_t& base, uint32_t& stride) { + if (attr >= 26) return false; + base = g_apiVtxArrays[attr][0]; + stride = g_apiVtxArrays[attr][1]; + return base != 0 && stride != 0; +} + // ============================================================================ // Vertex Descriptor // ============================================================================ extern "C" void GX__ClearVtxDesc_8016dc34() { + GX_DEFER_CALL0(GX__ClearVtxDesc_8016dc34); bool changed = false; for(int i=0; i<26; ++i){ changed |= g_hleGxState.vtxDesc[i] != GX_NONE; @@ -44,6 +55,7 @@ extern "C" void GX__ClearVtxDesc_8016dc34() { PPC_NATIVE_OVERRIDE_VOID(8016dc34, GX__ClearVtxDesc_8016dc34, (), ()); extern "C" void GX__SetVtxDesc_8016d3a4(uint32_t a, uint32_t t) { + GX_DEFER_CALL(GX__SetVtxDesc_8016d3a4, a, t); const uint32_t attr = CanonicalVtxAttr(a); if(attr>=26||attr==GX_VA_NULL) return; const GXAttrType oldType = g_hleGxState.vtxDesc[attr]; @@ -59,16 +71,22 @@ PPC_NATIVE_OVERRIDE_VOID(8016d3a4, GX__SetVtxDesc_8016d3a4, (uint32_t a, uint32_ extern "C" void GX__SetVtxDescv_8016d608(uint32_t la) { if(!la) return; uint32_t p=la; + std::vector> list; while(true){ uint32_t a=Memory::Read32(p); if(a==0xFFu) break; - GX__SetVtxDesc_8016d3a4(a, Memory::Read32(p+4)); + list.push_back({a, Memory::Read32(p+4)}); p+=8; } + GxWorker::Run([list = std::move(list)] { + for (const auto& [a, t] : list) GX__SetVtxDesc_8016d3a4(a, t); + }); } PPC_NATIVE_OVERRIDE_VOID(8016d608, GX__SetVtxDescv_8016d608, (uint32_t la), (la)); +// Display lists can change the vertex layout, so answer from the state the worker has applied. extern "C" void GX__GetVtxDesc_8016d9f0(uint32_t a, uint32_t tp) { + GX_SYNC_INLINE(); GXAttrType type = GX_NONE; const uint32_t attr = CanonicalVtxAttr(a); if (attr < GX_VA_MAX_ATTR && attr != GX_VA_NULL) { @@ -84,6 +102,7 @@ extern "C" void GX__GetVtxDescv_8016dba4(uint32_t la) { if (!la) { return; } + GX_SYNC_INLINE(); uint32_t p = la; for (uint32_t a = GX_VA_PNMTXIDX; a <= GX_VA_TEX7; ++a) { @@ -103,6 +122,7 @@ PPC_NATIVE_OVERRIDE_VOID(8016dba4, GX__GetVtxDescv_8016dba4, (uint32_t la), (la) // ============================================================================ extern "C" void GX__SetVtxAttrFmt_8016dc68(uint32_t vf, uint32_t a, uint32_t c, uint32_t t, uint32_t fr) { + GX_DEFER_CALL(GX__SetVtxAttrFmt_8016dc68, vf, a, c, t, fr); const uint32_t attr = CanonicalVtxAttr(a); if(vf<8&&attr<26){ const VtxAttrFmt oldFmt = g_hleGxState.vtxAttrFmt[vf][attr]; @@ -126,16 +146,21 @@ PPC_NATIVE_OVERRIDE_VOID(8016dc68, GX__SetVtxAttrFmt_8016dc68, (uint32_t vf, uin extern "C" void GX__SetVtxAttrFmtv_8016de08(uint32_t vf, uint32_t la) { if(!la) return; uint32_t p=la; + std::vector> list; while(true){ uint32_t a=Memory::Read32(p); if(a==0xFFu) break; - GX__SetVtxAttrFmt_8016dc68(vf, a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu); + list.push_back({a, Memory::Read32(p+4), Memory::Read32(p+8), Memory::Read32(p+12)&0xFFu}); p+=16; } + GxWorker::Run([vf, list = std::move(list)] { + for (const auto& [a, c, t, fr] : list) GX__SetVtxAttrFmt_8016dc68(vf, a, c, t, fr); + }); } PPC_NATIVE_OVERRIDE_VOID(8016de08, GX__SetVtxAttrFmtv_8016de08, (uint32_t vf, uint32_t la), (vf, la)); extern "C" void GX__GetVtxAttrFmt_8016e04c(uint32_t vf, uint32_t a, uint32_t cp, uint32_t tp, uint32_t fp) { + GX_SYNC_INLINE(); VtxAttrFmt fmt{}; const uint32_t attr = CanonicalVtxAttr(a); if (vf < 8 && attr < GX_VA_MAX_ATTR && attr != GX_VA_NULL) { @@ -157,6 +182,7 @@ extern "C" void GX__GetVtxAttrFmtv_8016e2b8(uint32_t vf, uint32_t la) { if (!la) { return; } + GX_SYNC_INLINE(); uint32_t p = la; for (uint32_t a = GX_VA_POS; a <= GX_VA_TEX7; ++a) { @@ -179,6 +205,11 @@ PPC_NATIVE_OVERRIDE_VOID(8016e2b8, GX__GetVtxAttrFmtv_8016e2b8, (uint32_t vf, ui // ============================================================================ extern "C" void GX__SetArray_8016e32c(uint32_t a, uint32_t ba, uint32_t str) { + if (!GxWorker::OnWorker()) { + const uint32_t attr = CanonicalVtxAttr(a); + if (attr < 26) g_apiVtxArrays[attr] = {ba, str}; + } + GX_DEFER_CALL(GX__SetArray_8016e32c, a, ba, str); const uint32_t attr = CanonicalVtxAttr(a); if(attr<26){ g_hleGxState.vtxArray[attr].base=ba; g_hleGxState.vtxArray[attr].stride=str; } } @@ -194,11 +225,13 @@ PPC_NATIVE_OVERRIDE_VOID(8016e1c4, GX__SetArray_8016e1c4, (uint32_t a, uint32_t // ============================================================================ extern "C" void GX__SetNumTexGens_8016e5a4(uint32_t n) { + GX_DEFER_CALL(GX__SetNumTexGens_8016e5a4, n); GXSetNumTexGens((u8)n); } PPC_NATIVE_OVERRIDE_VOID(8016e5a4, GX__SetNumTexGens_8016e5a4, (uint32_t n), (n)); extern "C" void GX__SetTexCoordGen2_8016e37c(uint32_t dc, uint32_t f, uint32_t sp, uint32_t m, uint32_t n, uint32_t pm) { + GX_DEFER_CALL(GX__SetTexCoordGen2_8016e37c, dc, f, sp, m, n, pm); if (sp >= static_cast(GX_MAX_TEXGENSRC)) { uint32_t pc = 0; uint32_t lr = 0; @@ -215,6 +248,7 @@ extern "C" void GX__SetTexCoordGen2_8016e37c(uint32_t dc, uint32_t f, uint32_t s PPC_NATIVE_OVERRIDE_VOID(8016e37c, GX__SetTexCoordGen2_8016e37c, (uint32_t dc, uint32_t f, uint32_t sp, uint32_t m, uint32_t n, uint32_t pm), (dc, f, sp, m, n, pm)); extern "C" void GX__EnableTexOffsets_8016f37c(uint32_t coord, uint32_t lineEnable, uint32_t pointEnable) { + GX_DEFER_CALL(GX__EnableTexOffsets_8016f37c, coord, lineEnable, pointEnable); GXEnableTexOffsets(static_cast(coord), lineEnable ? GX_TRUE : GX_FALSE, pointEnable ? GX_TRUE : GX_FALSE); @@ -222,11 +256,13 @@ extern "C" void GX__EnableTexOffsets_8016f37c(uint32_t coord, uint32_t lineEnabl PPC_NATIVE_OVERRIDE_VOID(8016f37c, GX__EnableTexOffsets_8016f37c, (uint32_t coord, uint32_t lineEnable, uint32_t pointEnable), (coord, lineEnable, pointEnable)); extern "C" void GX__SetLineWidth_8016f314(uint32_t width, uint32_t texOffsets) { + GX_DEFER_CALL(GX__SetLineWidth_8016f314, width, texOffsets); GXSetLineWidth(static_cast(width), static_cast(texOffsets)); } PPC_NATIVE_OVERRIDE_VOID(8016f314, GX__SetLineWidth_8016f314, (uint32_t width, uint32_t texOffsets), (width, texOffsets)); extern "C" void GX__SetPointSize_8016f348(uint32_t pointSize, uint32_t texOffsets) { + GX_DEFER_CALL(GX__SetPointSize_8016f348, pointSize, texOffsets); GXSetPointSize(static_cast(pointSize), static_cast(texOffsets)); } PPC_NATIVE_OVERRIDE_VOID(8016f348, GX__SetPointSize_8016f348, (uint32_t pointSize, uint32_t texOffsets), (pointSize, texOffsets)); @@ -274,6 +310,8 @@ void ServiceDeferredTimingDuringGxWork() { } } +static void GxBeginImpl(uint32_t t, uint32_t vf, uint32_t nv); + extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) { if(IsDisplayListActive()){ WriteDisplayListData((u8)(t|vf), 1); @@ -281,6 +319,11 @@ extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) { return; } ServiceDeferredTimingDuringGxWork(); + GX_DEFER_CALL(GxBeginImpl, t, vf, nv); + GxBeginImpl(t, vf, nv); +} + +static void GxBeginImpl(uint32_t t, uint32_t vf, uint32_t nv) { ApplyAuroraVtxStateForBegin(static_cast(vf)); g_hleGxState.currentVtxFmt=(GXVtxFmt)vf; g_hleGxState.currentPrim=(GXPrimitive)t; g_hleGxState.vertsRemaining=nv; g_hleGxState.inBegin=true; g_hleGxState.auroraBeginCalled=false; @@ -289,13 +332,19 @@ extern "C" void GX__Begin_8016f0f0(uint32_t t, uint32_t vf, uint32_t nv) { } PPC_NATIVE_OVERRIDE_VOID(8016f0f0, GX__Begin_8016f0f0, (uint32_t t, uint32_t vf, uint32_t nv), (t, vf, nv)); -extern "C" void GX__End_80044b30() { g_hleGxState.inBegin=false; GXEnd(); } +extern "C" void GX__End_80044b30() { + GX_DEFER_CALL0(GX__End_80044b30); g_hleGxState.inBegin=false; GXEnd(); } PPC_NATIVE_OVERRIDE_VOID(80044b30, GX__End_80044b30, (), ()); extern "C" void GX__End_80048c30() { GX__End_80044b30(); } PPC_NATIVE_OVERRIDE_VOID(80048c30, GX__End_80048c30, (), ()); extern "C" void GX__DrawSphere_80172a30(uint32_t numMajor, uint32_t numMinor) { + if (GxWorker::ShouldDefer()) { + EnsureAuroraFrameActive(); + GxWorker::Submit([=] { GX__DrawSphere_80172a30(numMajor, numMinor); }); + return; + } constexpr uint32_t kAttrCount = 26; constexpr float kSphereRadius = 1.0f; constexpr float kPi = 3.14159265358979323846f; diff --git a/runtime/src/hle/gx/gx_worker.cpp b/runtime/src/hle/gx/gx_worker.cpp new file mode 100644 index 0000000..c7dbd02 --- /dev/null +++ b/runtime/src/hle/gx/gx_worker.cpp @@ -0,0 +1,329 @@ +#include "gx_worker.h" + +#include "runtime_config.h" +#include "runtime_log.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif +#if defined(__x86_64__) || defined(_M_X64) +#include +#endif + +void GxWorkerReplayFifoWrite(uint32_t value, uint32_t sizeBytes); +void GxWorkerReplayFifoBurst(const uint8_t* data, uint32_t sizeBytes); + +namespace GxWorker { +namespace detail { +std::atomic g_active{false}; +constinit thread_local bool t_onWorker = false; +constinit thread_local int t_inlineDepth = 0; +bool g_fifoPending = false; +} // namespace detail + +namespace { +struct Header { + void (*run)(void* payload); + uint32_t size; + uint32_t reserved; +}; +constexpr size_t kHeaderSize = 16; +static_assert(sizeof(Header) == kHeaderSize); + +constexpr size_t kCapacity = size_t{8} << 20; +// How long the worker keeps polling before it sleeps; recording arrives in bursts and each wake-up +// costs the game thread a lock and notify. +constexpr auto kIdleSpin = std::chrono::microseconds{50}; +constexpr size_t kFifoFlushBytes = 8 * 1024; +constexpr uint8_t kFifoBurstTag = 0xB0; + +uint8_t* g_ring = nullptr; +uint64_t g_write = 0; +std::atomic g_head{0}; +std::atomic g_tail{0}; +std::atomic g_consumerWaiting{false}; +std::atomic g_producerWaiting{false}; +std::mutex g_headMutex; +std::condition_variable g_headCv; +std::mutex g_tailMutex; +std::condition_variable g_tailCv; +void (*g_waitCallback)() = nullptr; +std::atomic g_stop{false}; +std::thread* g_thread = nullptr; +std::vector g_fifoBytes; + +constexpr size_t AlignUp(size_t value) { return (value + detail::kAlign - 1) & ~(detail::kAlign - 1); } + +inline void CpuRelax() { +#if defined(__x86_64__) || defined(_M_X64) + _mm_pause(); +#endif +} + +// libc++ on Windows truncates sub-millisecond condvar timeouts to zero (a spin), so the bounded +// wait goes straight to the OS there. +void WaitTailBriefly(std::unique_lock& lock) { +#if defined(_WIN32) && defined(_LIBCPP_VERSION) + SleepConditionVariableSRW(reinterpret_cast(g_tailCv.native_handle()), + reinterpret_cast(lock.mutex()->native_handle()), 1, 0); +#else + g_tailCv.wait_for(lock, std::chrono::milliseconds{1}); +#endif +} + +void WaitForTail(uint64_t minTail, bool service) { + for (int spin = 0; spin < 256 && g_tail.load(std::memory_order_acquire) < minTail; ++spin) { + CpuRelax(); + } + while (g_tail.load(std::memory_order_acquire) < minTail) { + { + std::unique_lock lock{g_tailMutex}; + g_producerWaiting.store(true, std::memory_order_seq_cst); + if (g_tail.load(std::memory_order_seq_cst) < minTail) { + WaitTailBriefly(lock); + } + g_producerWaiting.store(false, std::memory_order_relaxed); + } + if (service && g_waitCallback != nullptr && g_tail.load(std::memory_order_acquire) < minTail) { + g_waitCallback(); + } + } +} + +void WaitForSpace(size_t bytes) { + if (g_write + bytes > kCapacity) { + WaitForTail(g_write + bytes - kCapacity, false); + } +} + +void ReplayFifo(void* payload) { + uint32_t size = 0; + std::memcpy(&size, payload, sizeof(size)); + const uint8_t* data = static_cast(payload) + sizeof(size); + const uint8_t* end = data + size; + while (data < end) { + const uint8_t tag = *data++; + if (tag == kFifoBurstTag) { + uint32_t length = 0; + std::memcpy(&length, data, sizeof(length)); + data += sizeof(length); + try { + GxWorkerReplayFifoBurst(data, length); + } catch (...) { + RT_LOGF(RT_TAG_GX, "GX worker FIFO burst failed\n"); + } + data += length; + } else { + uint32_t value = 0; + std::memcpy(&value, data, sizeof(value)); + data += sizeof(value); + try { + GxWorkerReplayFifoWrite(value, tag); + } catch (...) { + RT_LOGF(RT_TAG_GX, "GX worker FIFO write failed\n"); + } + } + } +} + +void WorkerMain() { + detail::t_onWorker = true; + AuroraSetGxProducerThread(); +#if defined(_WIN32) + SetThreadDescription(GetCurrentThread(), L"GX worker"); +#endif + uint64_t tail = g_tail.load(std::memory_order_relaxed); + for (;;) { + uint64_t head = g_head.load(std::memory_order_acquire); + if (head == tail) { + const auto spinUntil = std::chrono::steady_clock::now() + kIdleSpin; + while (head == tail) { + for (int spin = 0; spin < 64 && head == tail; ++spin) { + CpuRelax(); + head = g_head.load(std::memory_order_acquire); + } + if (head == tail && std::chrono::steady_clock::now() >= spinUntil) { + break; + } + } + } + if (head == tail) { + if (g_stop.load(std::memory_order_acquire)) { + return; + } + { + std::unique_lock lock{g_headMutex}; + g_consumerWaiting.store(true, std::memory_order_seq_cst); + while (g_head.load(std::memory_order_seq_cst) == tail && !g_stop.load(std::memory_order_acquire)) { + g_headCv.wait(lock); + } + g_consumerWaiting.store(false, std::memory_order_relaxed); + } + continue; + } + while (tail != head) { + uint8_t* record = g_ring + tail % kCapacity; + const Header* header = reinterpret_cast(record); + if (header->run != nullptr) { + try { + header->run(record + kHeaderSize); + } catch (const std::exception& error) { + RT_LOGF(RT_TAG_GX, "GX worker command failed: %s\n", error.what()); + } catch (...) { + RT_LOGF(RT_TAG_GX, "GX worker command failed\n"); + } + } + tail += header->size; + g_tail.store(tail, std::memory_order_seq_cst); + if (g_producerWaiting.load(std::memory_order_seq_cst)) { + std::lock_guard lock{g_tailMutex}; + g_tailCv.notify_all(); + } + } + } +} +} // namespace + +namespace detail { +void* Reserve(size_t payloadSize, void (*run)(void*)) { + const size_t total = kHeaderSize + AlignUp(payloadSize); + if (total > kCapacity) { + RT_LOGF(RT_TAG_GX, "GX worker command of %zu bytes exceeds the ring\n", total); + std::abort(); + } + size_t offset = static_cast(g_write % kCapacity); + const size_t remaining = kCapacity - offset; + if (total > remaining) { + WaitForSpace(remaining + total); + auto* wrap = reinterpret_cast(g_ring + offset); + wrap->run = nullptr; + wrap->size = static_cast(remaining); + g_write += remaining; + offset = 0; + } else { + WaitForSpace(total); + } + auto* header = reinterpret_cast(g_ring + offset); + header->run = run; + header->size = static_cast(total); + g_write += total; + return g_ring + offset + kHeaderSize; +} + +void Commit() { + g_head.store(g_write, std::memory_order_seq_cst); + if (g_consumerWaiting.load(std::memory_order_seq_cst)) { + std::lock_guard lock{g_headMutex}; + g_headCv.notify_one(); + } +} + +void FlushFifo() { + g_fifoPending = false; + if (g_fifoBytes.empty()) { + return; + } + const uint32_t size = static_cast(g_fifoBytes.size()); + void* payload = Reserve(sizeof(size) + size, &ReplayFifo); + std::memcpy(payload, &size, sizeof(size)); + std::memcpy(static_cast(payload) + sizeof(size), g_fifoBytes.data(), size); + g_fifoBytes.clear(); + Commit(); +} +} // namespace detail + +void FifoWrite(uint32_t value, uint32_t sizeBytes) { + const size_t at = g_fifoBytes.size(); + g_fifoBytes.resize(at + 1 + sizeof(value)); + g_fifoBytes[at] = static_cast(sizeBytes); + std::memcpy(g_fifoBytes.data() + at + 1, &value, sizeof(value)); + detail::g_fifoPending = true; + if (g_fifoBytes.size() >= kFifoFlushBytes) { + detail::FlushFifo(); + } +} + +void FifoBurst(const uint8_t* data, uint32_t sizeBytes) { + const size_t at = g_fifoBytes.size(); + g_fifoBytes.resize(at + 1 + sizeof(sizeBytes) + sizeBytes); + g_fifoBytes[at] = kFifoBurstTag; + std::memcpy(g_fifoBytes.data() + at + 1, &sizeBytes, sizeof(sizeBytes)); + std::memcpy(g_fifoBytes.data() + at + 1 + sizeof(sizeBytes), data, sizeBytes); + detail::g_fifoPending = true; + if (g_fifoBytes.size() >= kFifoFlushBytes) { + detail::FlushFifo(); + } +} + +void Sync() { + if (!Active() || detail::t_onWorker) { + return; + } + // Guest callbacks serviced while waiting can record more work. + do { + if (detail::g_fifoPending) { + detail::FlushFifo(); + } + WaitForTail(g_write, true); + } while (detail::g_fifoPending || g_tail.load(std::memory_order_acquire) != g_write); +} + +void SetWaitCallback(void (*callback)()) { g_waitCallback = callback; } + +void BeginInline() { + if (detail::t_inlineDepth == 0 && Active() && !detail::t_onWorker) { + Sync(); + } + ++detail::t_inlineDepth; +} + +void EndInline() { --detail::t_inlineDepth; } + +void Start() { + if (Active()) { + return; + } + const char* env = std::getenv("MKW_GX_WORKER"); + if (!RuntimeConfigFile::GxWorkerEnabled(true) || (env != nullptr && std::strcmp(env, "0") == 0)) { + RT_LOGF(RT_TAG_GX, "GX worker disabled\n"); + return; + } + if (g_ring == nullptr) { + g_ring = static_cast(::operator new(kCapacity, std::align_val_t{64})); + } + g_fifoBytes.reserve(kFifoFlushBytes + 4096); + g_stop.store(false, std::memory_order_relaxed); + g_thread = new std::thread(WorkerMain); + detail::g_active.store(true, std::memory_order_release); + RT_LOGF(RT_TAG_GX, "GX worker started\n"); +} + +void Stop() { + if (!Active()) { + return; + } + // Guest state may already be torn down; don't service it while draining. + g_waitCallback = nullptr; + Sync(); + detail::g_active.store(false, std::memory_order_release); + g_stop.store(true, std::memory_order_release); + Submit([] {}); + if (g_thread != nullptr) { + g_thread->join(); + delete g_thread; + g_thread = nullptr; + } +} + +} // namespace GxWorker diff --git a/runtime/src/hle/os/os_reset.cpp b/runtime/src/hle/os/os_reset.cpp index e36af5e..b6dbc53 100644 --- a/runtime/src/hle/os/os_reset.cpp +++ b/runtime/src/hle/os/os_reset.cpp @@ -1,5 +1,6 @@ #include "memory.h" #include "hle/audio/ax_dsp.h" +#include "gx_worker.h" #include "hle_stubs.h" #include "ppc_runtime.h" #include "runtime_log.h" @@ -124,6 +125,7 @@ extern "C" uint32_t OSResetSystem() std::fflush(stderr); // The AX mix worker holds resolved host pointers into the guest regions; // it must be stopped before those mappings are torn down. + GxWorker::Stop(); AxDspHle::ShutdownMixWorker(); Memory::Reset(); SetRuntimeExitCode(0); @@ -145,6 +147,7 @@ extern "C" uint32_t Exit_801AE58C(int status) ShowRuntimeFatalPopup("the game exited with an error status", details); MarkFatalErrorReported(); } + GxWorker::Stop(); SetRuntimeExitCode(status); std::exit(status); return static_cast(status); diff --git a/runtime/src/hle/vi.cpp b/runtime/src/hle/vi.cpp index 7ee76b5..4ae7ade 100644 --- a/runtime/src/hle/vi.cpp +++ b/runtime/src/hle/vi.cpp @@ -577,7 +577,7 @@ void VI_HLE_PresentFrame(bool presentedXfb, bool paceToRetrace) { aurora_set_present_schedule(0, 0); } - aurora_end_frame(); + EndAuroraFrame(); if (paceThisFrame) { PaceToRetraceBoundary(paceDeadline); std::lock_guard lock(g_viMutex); @@ -751,7 +751,10 @@ extern "C" void VIConfigure_HLE_801b9f6c(CpuContext* ctx) renderMode.xfbHeight != 0 ? renderMode.xfbHeight : renderMode.efbHeight; } - ::VIConfigure(&renderMode); + { + GX_SYNC_INLINE(); + ::VIConfigure(&renderMode); + } ViSetR3(ctx, 0); } diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 249f03e..4f4adde 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -66,6 +66,7 @@ #include "wiimote_host.h" #include "discord_presence.h" #include "fiber_manager.h" +#include "gx_worker.h" #include "hle_stubs.h" #include "runtime_config.h" #include "runtime_log.h" @@ -88,6 +89,9 @@ namespace { std::string FormatHostStackTrace(unsigned framesToSkip = 0); void ServiceGuestTimingDuringAuroraFrameWait() { + if (GxWorker::OnWorker()) { + return; + } // Aurora can block inside FIFO drains before control returns to GX HLE, for as long as a // whole display period. Keep VI retraces, alarms and audio moving at wall-clock cadence here // while still suppressing guest rescheduling and recursive Aurora work. @@ -1466,6 +1470,8 @@ int RuntimeMain(int argc, char** argv) { << (forceAspect169 ? "16:9" : (configWidescreen ? "surface (dynamic EGG canvas)" : "4:3")) << std::endl; g_auroraInitialized.store(true, std::memory_order_release); + GxWorker::SetWaitCallback(ServiceGuestTimingDuringAuroraFrameWait); + GxWorker::Start(); auto entry = ResolveEntry(); InitializePersistentCpuContext(); @@ -1494,6 +1500,7 @@ int RuntimeMain(int argc, char** argv) { Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); Wiimote::Shutdown(); + GxWorker::Stop(); aurora_shutdown(); DiscordPresence::Shutdown(); SetRuntimeExitCodeImpl(0); @@ -1513,6 +1520,7 @@ int RuntimeMain(int argc, char** argv) { Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); Wiimote::Shutdown(); + GxWorker::Stop(); aurora_shutdown(); DiscordPresence::Shutdown(); ShutdownProcessTranscript(); @@ -1526,6 +1534,7 @@ int RuntimeMain(int argc, char** argv) { Fiber::GuestFiberManager::Shutdown(); WindowPlacementPersistence::Flush(true); Wiimote::Shutdown(); + GxWorker::Stop(); aurora_shutdown(); DiscordPresence::Shutdown(); ShutdownProcessTranscript(); diff --git a/runtime/src/settings_overlay.cpp b/runtime/src/settings_overlay.cpp index d4129c8..d990acc 100644 --- a/runtime/src/settings_overlay.cpp +++ b/runtime/src/settings_overlay.cpp @@ -1,4 +1,5 @@ #include "settings_overlay.h" +#include "gx_worker.h" #include "audio_backend.h" #include "aurora_events.h" #include "controller_button_names.h" @@ -188,6 +189,7 @@ bool IsHighFrameRateMode() { } void SetResolutionScale(float scale) { + GX_SYNC_INLINE(); g_resolutionScale = scale; VISetFrameBufferScale(scale); RuntimeConfigFile::SetResolutionMultiplier(scale); @@ -1134,6 +1136,7 @@ void DrawGraphicsSettings() { ImGui::EndCombo(); } if (frameInterpolationModeChanged) { + GX_SYNC_INLINE(); const uint32_t targetFps = kFrameInterpolationTargetFps[static_cast(g_frameInterpolationMode)]; aurora_set_frame_interpolation_fps(targetFps); RuntimeConfigFile::SetFrameInterpolationFps(targetFps); @@ -1177,10 +1180,12 @@ void DrawGraphicsSettings() { } #endif if (ImGui::Checkbox("Disable copy filter", &g_disableCopyFilter)) { + GX_SYNC_INLINE(); aurora_set_disable_copy_filter(g_disableCopyFilter); RuntimeConfigFile::SetDisableCopyFilter(g_disableCopyFilter); } if (ImGui::Checkbox("Skip draws while shaders compile", &g_skipUnreadyPipelines)) { + GX_SYNC_INLINE(); aurora_set_skip_unready_pipelines(g_skipUnreadyPipelines); RuntimeConfigFile::SetSkipUnreadyPipelines(g_skipUnreadyPipelines); } @@ -1573,9 +1578,7 @@ void ReleaseControllers() noexcept { } void Draw() noexcept { - // Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists - // and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands. - aurora_wait_for_frame_worker(); + // ImGui is only touched on this thread; the render worker replays a frozen copy. ApplyConfiguredMappings(); PersistDisplayModeIfChanged(); UpdateCursorAutoHide();