Skip to content

video_core: keep karts on the water and restore waves (Mario Kart World) - #690

Merged
capy-ai[bot] merged 2 commits into
nx2from
capy/port-mkw-water-collision
Nov 8, 2026
Merged

capy-ai[bot] merged 2 commits into
nx2from
capy/port-mkw-water-collision

Conversation

@zortos293

Copy link
Copy Markdown
Owner

Ports the water half of upstream Eglantine's Mario Kart World water/movie fix and makes its collision write-back stop stalling the GPU thread.

What it fixes in game

Mario Kart World (0400C3F00006E000). Two related water bugs:

  • Kart sinks below the water surface. The first water section after loading behaves, later ones sink (sometimes after a few seconds), and pausing once fixes the rest of the course. The game reads a CPU-side 256x256 collision height map that a compute pass (native key 6e0ae200ee9aa220) writes, but nothing writes that image back to guest memory: the texture cache only writes back GPU-modified images on request, and nx2's read traps cover buffers, not images.
  • Flat water, no waves (upstream issue shader: bind loaded pointers that compete with a paired cbuf pointer (OoT near foliage) #410, e.g. Wario Shipyard). When the wave shader (native key 5a4f3726590efb17) gets an empty water/ground depth pair, its depth factor is 0 and the displacement disappears. The fallback sets the factor to 1.0 only when both inputs are exactly zero, so real shallows and dry ground keep their behaviour.

Source

What I adapted (commit 1, cherry-pick)

  • Left out the movie-pacing half (upstream [Crash] Emulator won't launch #729, @klova5 447c9244a9, which skips present pacing while a large MKW movie plays). klova5 diagnosed the slow cup-intro movie: the game requested decoded frames only ~34 times a second while vi2's present blocked. That is the same mechanism Lichii nx2: keep movie video in sync with its audio (imports #571, ports upstream 1f52ac7e2e) #659 fixed at the root: am.cpp now serves the gsv/vi2/vdec sessions on three host threads, and upstream doesn't have that fix. A second, game-specific pacing bypass isn't needed unless the intro is still slow on current nx2. See the test steps below.
  • Left out the mkw_zero_depth_v1 pipeline cache directory. nx2 keys a native program's pipeline by the hash of its lowered code (GenericEnvironment::Analyze → NativeShaderHash). The patched shader gets a new key, so the cached unpatched pipeline is never used. Moving the directory would discard every MKW user's shader cache and break Qt's "remove shader cache" paths for MKW.
  • Moved the dispatch hook to where nx2's DispatchCompute defines qmd. The include was adjusted to nx2's list.

What I changed on top (commit 2)

  • No GPU wait per dispatch. Upstream calls runtime.Finish() after every dispatch of the collision program, so on water courses the GPU thread waits for the GPU to go idle at least once per frame. This version queues the image on the existing preemptive download path, the same uncommitted_downloads path BindRenderTarget uses. The next fence records the copy, and the fencing thread writes it to guest memory when the GPU reaches that fence. nx2 signals a fence per compute dispatch, so the copy is committed right away. Trade-off: with the default (fast) GPU fence behaviour the guest's semaphore release happens immediately, so the game may read a map that is up to one GPU latency (about a frame) old. Upstream's Finish() gives it the same-frame map. The bug being fixed is a map that is never updated, so a one-frame lag should not matter, but it is not proven in game.
  • Branch guard for the wave fallback. The fallback inserts three instructions and shifts everything after them. Upstream checked that its lowering of this shader has no branches. nx2's lower_to_maxwell.h differs a lot, so the fallback now skips (and logs a warning once per lowering) if the lowered program has any branch, call, or SSY/PBK/PCNT push (Maxwell 0xe20–0xe2b), instead of corrupting their targets.

How I verified it

  • Linux GCC 13 Release with -DENABLE_WERROR=ON (the CI configuration, Qt off): tests target, which also builds video_core and core, built with 0 warnings. Full ./bin/tests: all 889 test cases passed.
  • aarch64: clang++ --target=aarch64-linux-gnu -fsyntax-only with each TU's compile_commands flags passed for shader_environment.cpp, vk_pipeline_cache.cpp, vk_rasterizer.cpp and vk_texture_cache.cpp (which instantiates texture_cache.h). For vk_rasterizer.cpp the host's x86-64 ffmpeg headers were put on the include path.
  • No conflict markers, git diff --check clean.

Not verified

Nothing ran in the game. I couldn't check whether nx2's lowering of the wave shader has branches (if it does, the log shows the warning below and waves stay flat), whether the async write-back is fresh enough for the game's physics, or the fps change.

How to test in game

  1. Grand Prix or Free Roam on a course with several water sections (Wario Shipyard, or any beach/harbour course). Drive through the second and later water sections without pausing. Expected: the kart floats on every section instead of sinking.
  2. On Wario Shipyard, look at the open water and the whirlpool. Expected: visible waves instead of flat water.
  3. Search the log for Mario Kart World wave fallback skipped. If it's there, the wave half did nothing on this build; report it so it can be redone for nx2's lowering.
  4. FPS on a water course compared with current nx2 should be unchanged. This PR adds no GPU wait.
  5. While you're there, time a cup intro movie (the flyover before a Grand Prix, about 24 s, 1452 frames). If it plays at full speed with clean sound on nx2, the upstream movie-pacing bypass isn't needed. With *:Info, nx2:vdecsvc frame 1 (2560x1440 and frame 1452 should be about 24 s apart.

Co-authored-by: Codex draft codex-draft@users.noreply.github.com

Open in Capy

snowstormxd and others added 2 commits November 7, 2026 23:44
Port the fingerprinted wave and collision-map fixes from #553 and adapt #729 movie pacing without opt-in switches. Limit movie activity to MKW and the current graphics session, track completed frame outputs independently of release fences, and retain current experimental decoding and shader diagnostics.

Assisted by Codex.

(cherry picked from commit 39a33c4)

Lichii port (Eglantine-Community/Eglantine#737, merged to upstream nx2 through #755):
- Only the water half is taken: the wave displacement fallback for
  empty depth inputs (upstream #553, 9530cecfc4) and the collision height map
  write-back (upstream #553, 13c88a9599). The movie pacing half (upstream #729, klova5's
  447c9244a9) is left out: nx2 serves the gsv/vi2/vdec sessions on three
  host threads (am.cpp, Lichii #659), the root cause of the game asking for
  decoded frames ~34 times a second while vi2 blocked its only thread.
- The mkw_zero_depth_v1 pipeline cache directory is left out. nx2 keys a
  native program's pipeline by the hash of its lowered code
  (GenericEnvironment::Analyze), so the patched shader gets a new key and
  the cached unpatched one is never used; moving the directory would only
  throw away every Mario Kart World cache entry and break the Qt
  "remove shader cache" paths for it.
- The dispatch hook is placed after nx2's DispatchCompute setup, where
  qmd is defined, and the include follows nx2's list.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…GPU wait

The ported write-back downloaded the 256x256 collision height map and
called runtime.Finish() after every dispatch of the collision program,
so the GPU thread waited for the GPU to go idle at least once a frame on
any course with water. Queue the image on the existing preemptive
download path instead (the one BindRenderTarget uses): the next fence
records the copy, and the fencing thread writes it to guest memory when
the GPU reaches that fence. nx2 signals a fence per compute dispatch, so
the map lands within the GPU's own latency, without the stall.

The wave fallback inserts three instructions into the lowered program
and moves everything after them. Upstream checked that its lowering of
this shader has no branches; nx2's lowering differs, so skip the
fallback (with a warning) if the lowered program has any branch, call or
SSY/PBK/PCNT push, instead of corrupting its targets.

Follows up Eglantine-Community/Eglantine@39a33c4a78 (upstream #737).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 78327339-16a9-40af-a0ef-6f14c19acec1
Commits

Reviewing files that changed from the base of the PR and between bf4c42e and 23f60ff.

Files selected for processing: 7

  • src/video_core/mkw_wave_workaround.h
  • src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
  • src/video_core/renderer_vulkan/vk_pipeline_cache.h
  • src/video_core/renderer_vulkan/vk_rasterizer.cpp
  • src/video_core/shader_environment.cpp
  • src/video_core/texture_cache/texture_cache.h
  • src/video_core/texture_cache/texture_cache_base.h
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

View 2 other findings in Capy.

Open in Capy

@capy-ai
capy-ai Bot merged commit 5acbea8 into nx2 Nov 8, 2026
5 checks passed
jpkto added a commit to jpkto/Lichii that referenced this pull request Nov 8, 2026
Brings in the 23 nx2 commits from zortos293#792 to zortos293#621 (bf48fe9). Thirteen files conflicted.

Two changes of this branch are now upstream and are dropped here:
- "cmake: reuse a cached CI package when its checksum can't be downloaded" (f3702f3). zortos293#792
  records the hash an archive was verified against next to the cached package and uses it
  without downloading the sha512sum. CPMUtil.cmake is upstream's. Ours covered one more case: a
  cache made before zortos293#792, at the first configure after it, with the release host down. That
  configure fails as before zortos293#792; the first one that reaches the host records the hash.
- "vulkan: keep a deferred clear's render pass for the draws after it" (1457bd0). zortos293#699
  continues the active pass for every draw on the same framebuffer and render area; ours did
  that only for a clear variant without MSAA discards. Without tiler resolves (desktop GPUs)
  every deferred clear's variant is such a variant, so the two keep the same passes. On tilers
  zortos293#699 also stays in a variant that discards its MSAA attachments, where ending it, as ours did,
  loaded attachments that pass had just discarded. RequestRenderpass is upstream's, and
  Scheduler's clear_variant member is gone.

The other conflicts keep both sides:
- vk_rasterizer.h: this branch's read entry points (FlushRegionForRead, FinishRead, TraceRead)
  next to zortos293#752's pending_tick parameter and IsGpuTickDone.
- vk_rasterizer.cpp, DispatchCompute: zortos293#690 adds publish_collision_map next to
  record_read_barrier, which this branch replaced ("vulkan: record a dispatch's barrier in the
  dispatch's own command"). publish_collision_map stays, record_read_barrier stays removed.
- vk_rasterizer.cpp, GetFlushArea: zortos293#264 forgets the read traps over a texture cache area, this
  branch tags the area's source for the read statistics. Both.
- buffer_cache.h: zortos293#816 marks native compute bindings only as far as their stores reach, this
  branch passes the write site to MarkWrittenBuffer. Both, in BindMultiRangeStorage and
  BindHostComputeStorageBuffers.
- buffer_cache_base.h: Nx2ReadListCopy's comment keeps this branch's word-exact description and
  adds zortos293#752's Pending.
- dma_pusher.h, dma_pusher.cpp: zortos293#752 suspends a channel on a list copy the host GPU hasn't
  finished and skips the writer wait when the entry is parsed again. This branch takes
  GPU-written macro parameters as placeholders (TakeGpuWrittenListWords), waits only for the
  writers before the last sync (WaitForSyncedWriters) and counts list reads
  (EDEN_NX2_LIST_STATS). Step tries the placeholders first and then zortos293#752's read, which may
  suspend. A resumed read skips WaitForSyncedWriters: it ran before the suspension. The read
  statistics wrap the pending-aware read.
- control/scheduler.h, scheduler.cpp, gpu_thread.cpp: zortos293#621 makes ResumePending return whether a
  channel moved, to reset the poll timer, as this branch's "video_core: decide Switch 2 acquires
  from the split's semaphore view" (188baa1) did. Upstream's names are taken. The poll's
  profile zone, PublishSoon and the QMD view takeover after the poll stay.
- puller.cpp: zortos293#687 releases fences again, at most once a millisecond, while an acquire waits
  with delayed fence operations. This branch split CanResumeAcquire into the semaphore view
  (PollWithoutDrain) and the drained path, and moved the long-wait log into ReportLongWait.
  zortos293#687's release goes into the drained path before that log, where upstream has it. The view
  needs none: it applies only while releases land at once (ReleasesLandAtOnce), never with
  delayed fence operations.
- maxwell_3d.cpp: zortos293#488 tracks whether the back's method sink holds state writes (sink_pending),
  this branch sends the same writes with Writes::Noted and notes what they write. Both. A back
  macro consumes the sink before it runs, as ProcessMacro does, so sending one clears
  sink_pending too.

Interactions checked:
- zortos293#816 and the GPU-written list words: a dispatch from list words (Nx2ListIndirect) has its
  slot's placeholders as its grid, small numbers in y and z. BindComputeStorageBuffer resolved
  the store extent with them, which marks too few bytes when a store's offset grows with the
  group id in y or z: list words and read downloads over the rest would have gone stale. Such
  a dispatch's grid now counts as unknown, as an indirect dispatch's does. Everywhere else the
  mark leaves out only bytes no store reaches, which no list word or read download depends on.
- zortos293#752 and the word-exact list reads: Nx2ReadListCopy returns Pending only after its word
  checks, so lists on words no GPU work wrote still read guest memory without waiting.
  EDEN_NX2_LIST_VERIFY reads its reference as before (all writers, waiting for the copy).
  EDEN_NX2_ENTRY_CHECK and EDEN_NX2_DRAIN_CHECK look at entries on pages no GPU work wrote,
  which never suspend on a copy.
- zortos293#264 and the read downloads: every GetFlushArea path a Switch 2 title takes (read downloads in
  flight, early downloads, flushes) forgets the read's traps, so the next GPU write arms them
  again; the texture cache's area now does too.
- zortos293#337 and "land read downloads under query results": a fence-flagged report's operation still
  lands the read downloads under it first, now from the fence thread, which holds no cache lock
  while it runs operations. zortos293#337 also makes such reports land at their fence with Normal
  accuracy. The semaphore view values the payload reports the front sends itself without
  reading memory. A word it reads from memory once the back ran the release may still lack a
  report waiting for its fence: the channel then waits until that report's operation latches it.
- zortos293#488 and "engines: write 3D state registers in one call": the back still puts state writes
  outside macros in the method sink and consumes it before macro writes, executable methods and
  method runs. The fast path only changes how CallMethod writes a register, so sink_pending
  matches the back.
- zortos293#688 and the back macros: native MME no longer reads its parameters again from guest memory.
  The back's macro engine runs on the parameters the front fetched, as the front's does.
- zortos293#621 and "release threads waiting for sync operations once the GPU threads stop": the poll
  only resets its timer and skips the sleep when no channel is left; NotifyShutdown is unchanged.
- zortos293#769 pins the split GPU threads to their own cores by default (8 physical cores on a 7800X3D):
  the threads this branch measured unpinned are pinned now.
- zortos293#818, zortos293#817, zortos293#296 and zortos293#690 change image uploads and the texture cache. None of them uses a
  buffer cache buffer on the GPU, so the upload reordering's use marks still cover every buffer
  use. zortos293#815, zortos293#781 and zortos293#820 change only the pipeline cache and shaders, zortos293#655 the game patches and
  the vsync rate (60 Hz by default), zortos293#689 decoder frames.

Tested:
- Compile checks of 23 files: the conflicted ones, the auto-merged ones both sides changed and
  users of the changed headers, without RTTI. The tests that include the changed headers
  compile (buffer_cache_ports, command_snapshot_suspensions, command_writer,
  maxwell_fast_method, gpu_written_pages).
- clang strict warnings (shadowing, narrowing, sign, switch, reorder) on the lines changed
  against bf48fe9 and against f9f3bc6: none.
- The harnesses, rebuilt against this tree (their settings objects too, since zortos293#655, zortos293#769 and
  zortos293#816 add settings):
  - split: 20 of 20 runs, and 10 of 10 pinned to one CPU;
  - QMD view, with EDEN_NX2_DRAIN_CHECK=1 EDEN_NX2_ACQUIRE_STATS=1: no failures, every drain
    check released otherwise 0, read otherwise 0, differing 0;
  - read downloads (with buffer_cache_ports' download, read and GPU write cases): 14,311
    checks, no failures;
  - DMA writes: no failures;
  - list writers: entry (EDEN_NX2_DRAIN_CHECK=1 EDEN_NX2_ENTRY_CHECK=1) 17 checks, tagged
    212,633, front 158, vk 288 (lavapipe with synchronization validation: nothing outside the
    controls, each control reports its hazard), no failures. The macro harness needs
    nx2_list_dump.h, which neither this tree nor the PR head has, and was not run;
  - upload reordering: hreorder and hshutdown, no failures;
  - gpu_hbatch (lavapipe with synchronization validation): nothing outside the controls, each
    control reports its hazard, no failures.
  The output of every harness that ran matches its run on the PR head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pavelgajdo pushed a commit to pavelgajdo/Lichii-Android that referenced this pull request Nov 8, 2026
…ld) (zortos293#690)

* mkw: enable targeted water feedback (Mario Kart World)

Port the fingerprinted wave and collision-map fixes from zortos293#553 and adapt zortos293#729 movie pacing without opt-in switches. Limit movie activity to MKW and the current graphics session, track completed frame outputs independently of release fences, and retain current experimental decoding and shader diagnostics.

Assisted by Codex.

(cherry picked from commit 39a33c4)

Lichii port (Eglantine-Community/Eglantine#737, merged to upstream nx2 through zortos293#755):
- Only the water half is taken: the wave displacement fallback for
  empty depth inputs (upstream zortos293#553, 9530cecfc4) and the collision height map
  write-back (upstream zortos293#553, 13c88a9599). The movie pacing half (upstream zortos293#729, klova5's
  447c9244a9) is left out: nx2 serves the gsv/vi2/vdec sessions on three
  host threads (am.cpp, Lichii zortos293#659), the root cause of the game asking for
  decoded frames ~34 times a second while vi2 blocked its only thread.
- The mkw_zero_depth_v1 pipeline cache directory is left out. nx2 keys a
  native program's pipeline by the hash of its lowered code
  (GenericEnvironment::Analyze), so the patched shader gets a new key and
  the cached unpatched one is never used; moving the directory would only
  throw away every Mario Kart World cache entry and break the Qt
  "remove shader cache" paths for it.
- The dispatch hook is placed after nx2's DispatchCompute setup, where
  qmd is defined, and the include follows nx2's list.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* texture_cache: write Mario Kart World's collision map back without a GPU wait

The ported write-back downloaded the 256x256 collision height map and
called runtime.Finish() after every dispatch of the collision program,
so the GPU thread waited for the GPU to go idle at least once a frame on
any course with water. Queue the image on the existing preemptive
download path instead (the one BindRenderTarget uses): the next fence
records the copy, and the fencing thread writes it to guest memory when
the GPU reaches that fence. nx2 signals a fence per compute dispatch, so
the map lands within the GPU's own latency, without the stall.

The wave fallback inserts three instructions into the lowered program
and moves everything after them. Upstream checked that its lowering of
this shader has no branches; nx2's lowering differs, so skip the
fallback (with a warning) if the lowered program has any branch, call or
SSY/PBK/PCNT push, instead of corrupting its targets.

Follows up Eglantine-Community/Eglantine@39a33c4a78 (upstream zortos293#737).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: snowstormxd <214507868+snowstormxd@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
(cherry picked from commit 5acbea8)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants