Skip to content

Ocarina of Time outdoors +15%: Occlusion Off and cheaper kernel locks from Zortos #717 - #16

Open
javocsoft wants to merge 36 commits into
nx2from
perf/oot-717-cpu
Open

javocsoft wants to merge 36 commits into
nx2from
perf/oot-717-cpu

Conversation

@javocsoft

Copy link
Copy Markdown
Owner

Stacked on #15 (which contains #14 and #13): merge those first and this shows its own 3 commits.

Ocarina of Time outdoors (Kokiri Forest) goes from about 25 to 29 fps on an RTX 3050 Laptop. This comes from the two parts of Zortos zortos293#717 that pay off on this tree, ported without its environment switches.

What and why

Zortos zortos293#717 (stephenjayakar) is one squashed commit of +15,000 lines with 153 EDEN_NX2_* switches. Built as-is with GCC 13 here, it ran Kokiri Forest at 32.0 fps against 25.7 for this tree. Turning off one group of its switches at a time, and then porting the CPU side onto this tree and repeating that per piece, gave:

Piece Kokiri Forest, fps without → with Taken
Occlusion Off (built-in patch, cLohan's IPS) 24.9 → 28.3 yes
Kernel locks, waits and memory traps 27.9 → 29.4 yes
Native zstd, fast string lookup, resource poll backoff 29.0 / 29.4 / 28.5 → 28.3 no: no difference
Widened thread affinity for Ocarina's main threads 29.4 → 29.4 no
JIT group 29.9 → 32.0 in the PR no: its fold-address / zero-extend / scratch-copy / GPR-spill group makes the game read unmapped memory
GPU side (buffer cache 1.8, split 0.6, back caches 0.3 fps in the PR) not yet

docs/ai/investigations.md has the numbers.

Measured, this branch (b30) against #15 (b28)

Alternating runs from the same save, RTX 3050 Laptop, NoSharpening and CPU ASTC:

  • Kokiri Forest, half resolution, 3 rounds: 25.4 → 29.3 fps (+15%).
  • Link's house, 1x, 5 rounds: 35.8 → 35.6 fps (no change).
  • Same 24 log errors, no guest unmapped reads, screenshots match apart from the leaves and fireflies that move.

Occlusion Off appears in Add-Ons like the other built-ins and can be turned off there. It applies only to that build's main, and only if all six words are the original ones.

Checked

  • build-tests.sh gcc: all 6 test programs pass, with the new builtin_patches.cpp cases and kernel_locks.cpp. The gate passes.

Not checked

  • Windows, macOS and Android hosts. The kernel changes avoid platform code, but only Linux ran them.
  • Other games: only the kernel part applies to them.

🤖 Generated with Claude Code

zortos293 and others added 30 commits November 8, 2026 00:02
…ports upstream 1f52ac7) (zortos293#659)

* fsp: no SD card read throttle for Switch 2 titles

IStorage::Read sleeps to emulate a 40 MB/s Switch 1 card, charging up to
64 KiB per read (Eden cbd4cf2, for Mario Strikers' intro). Switch 2
storage is many times faster. Ocarina of Time streams its title movie with
300-460 reads a second on the thread that also returns decoder images, so
that thread slept for much of each second. Switch 1 titles keep the
throttle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* am: serve the Switch 2 graphics system on three host threads

nx2:gsv comes from ISelfController, so it and every session it hands out
(vi2, vdecsvc, syncpt, gpu, nvsrv) run on AM's server, which had one host
thread. vi2's PresentClientBuffer blocks until the frame's fence and the
next vblank, about 14 ms of each 16.7 ms frame at 60 fps, and every other
request queued behind it. In Ocarina of Time the movie thread's decoder
calls took 1.2 ms each instead of 0.1 ms; it returned decoder images at
about 33 a second while presenting at 60, and the title movie fell 10 s
behind its audio. With two more threads it falls 4 s behind, all at
start-up; the rest is the input queue (next commit in the series).

Each service keeps its own lock, as with bsdsocket's extra threads.
ServerManager::LoopProcess now joins its additional threads before it
returns, so state a server keeps on its caller's stack (AM's WindowSystem)
outlives any request they are still handling at shutdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* core: refuse video decoder packets with nn::codec 3603 when the input queue is full

The SDK refuses a packet with nn::codec 3603 when the decoder cannot take it,
and the client retries after the submission-ready event. Ocarina of Time's
movie player feeds DecodeAsync until it fails. Accepting every packet let it
queue its whole title movie (~2,600 packets in 20 s); its pictures came at
20-28 fps until then and the video started ~11 s behind its audio.

SubmitInput now refuses from 24 queued packets, 3x the readiness watermark of
8. Players that send one packet per ready signal and ignore the result (Mario
Tennis Fever, zortos293#49) never reach it, because the auto-clear event stops at 8.
End of stream is never refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* core: document Nx2VideoInputQueue::Full

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* nx2: drop late movie frames so the picture keeps up with the sound

Ocarina of Time shows one decoded frame per frame it draws and never
skips one, while the movie's audio plays in real time. Below 60 fps the
picture falls further behind the sound with every frame: in a user's
log the opening cutscene (7040 frames, 117 s) took 148 s at 48 fps and
ended about 30 s behind its audio. The title video falls behind the same
way (Eglantine-Community/Eglantine#629).

The video session now keeps a movie clock, started from the frames the
game acquires. When an output image comes free, decoded frames that a
later frame has already overtaken on that clock are dropped without
being converted or written, so the next image holds the frame the sound
is at. Frames are only dropped, never held back, and never the
end-of-stream frame. A gap of over 1 s between acquires (a pause) or a
lag over 3 s restarts the clock instead, as do flush, stop, finalize and
a new configure. Streams without rising timestamps are left alone.

At full speed nothing is dropped. EDEN_NX2_VDEC_PACE=0 turns it off.
The decoder log line now counts the dropped frames.

Assisted-by: Claude Opus 5.5 (debugging, implementation, commit message draft)
Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>

* nx2: log how far movie frames lag their timestamps

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

* nx2: pace movies unconditionally (no EDEN_NX2_VDEC_PACE switch)

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

* service: lock domain handler tables and applet messages for AM's extra host threads

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

* nx2: restart the movie clock after an emulator pause; am: lock the context stack and user channel

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

* service: read the domain flag atomically

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

---------

Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Co-authored-by: BlAnK182C-bak <207436459+BlAnK182C-bak@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
…urations again (Mario Kart World 30 fps lock) (zortos293#664)

* video_core: scale Ampere GPU time since the last present under Boost and Overclock

Since zortos293#84, Switch 2 report timestamps are CoreTiming nanoseconds and the
GPU Clocks setting no longer affects them. Ocarina of Time sizes its
dynamic resolution from GPU marker durations within a frame, and
emulated GPU work measured in host wall-clock time keeps it at 854x480.

Boost now divides the time since the last present by 4 and Overclock by
16; Normal keeps exact nanoseconds. A timestamp never runs ahead of
CPU time and trails it by less than the time since that present, and it
stays monotonic when the anchor moves, so CPU/GPU calibrations such as
Star Fox's frame throttle keep the same clock domain. With a divisor of
4 (the EDEN_NX2_GPU_TIME_DIV experiment) Ocarina rose to 1920x1080.

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

* video_core: latch the GPU clock with each present and keep Ampere time exact before the first one

GPU Clocks can change while a game runs. Reading it per report let a
change mid-frame move timestamps backward (Normal at P + 12 ms gave
P + 12 ms, Boost at P + 13 ms gave P + 3.25 ms). The clock is now
latched with the present time in one atomic word, so a change applies at
the next present, where timestamps stay monotonic.

Before the first present the anchor was zero, so Boost returned host
uptime / 4 while nvdrv GetGpuTime returned full uptime. Timestamps are
now exact until the first present.

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

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
… (EGLA-67) (zortos293#679)

Imports the EASY AMD fixes from the CheezerWheezer/Lichii survey (EGLA-67): occlusion query last-query wait on AMD, non-Dref compare sampler variant, blend key rebuild without EDS3 blend, null texture for out-of-range TIC handles, pipeline cache reset on driver rejection, zortos293#448 uneven BCn mips stay RGBA8 on AMD, zortos293#555 preloaded pipelines kept from eviction, docs/nx2/amd.md (zortos293#437). Not run on AMD hardware.

Co-authored-by: conongnang <conongnang@users.noreply.github.com>
Co-authored-by: chuppatricky <74110004+chuppatricky@users.noreply.github.com>
Co-authored-by: sharkiix <Shakoquo@users.noreply.github.com>
Co-authored-by: Erick <ezava85@gmail.com>
Co-authored-by: persluffy-cl <hansgamers1999@gmail.com>
Co-authored-by: ne1i <valentinfroment7@gmail.com>
Co-authored-by: EtienneDefranc <156627201+EtienneDefranc@users.noreply.github.com>
… 2, AMD fixes

Three commits Zortos merged since the last sync:
- zortos293#659 is Zortos's version of the movie sync this tree took from zortos293#571 in #12. It also brings what
  serving AM on three host threads needs and zortos293#571 lacked: sessions cloned from one domain share a
  manager, so domain handlers are now guarded (appending one returned a count another thread
  could change); the applet's launch parameters, message queue and context stack are taken under
  its lock; and CoreTiming's pause end time is atomic. Its movie clock (emulated time, pause
  aware) is left out: this tree's late-frame dropping keeps Ocarina's title movie in time here
  (0.15 s at 20 s), so nx2_gsv.h stays as it was, with the same decoder backpressure.
- zortos293#664 makes GPU Clocks Boost and Overclock scale Switch 2 GPU durations again (Normal is exact):
  Ocarina sizes its dynamic resolution from them, and under Boost it renders at 1920x1080 instead
  of 854x480.
- zortos293#679 imports AMD fixes (occlusion query wait, compare samplers, blend keys without EDS3 blend,
  null textures), documented in docs/nx2/amd.md. Nothing here runs on AMD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starting a game with a missing mod root could dereference a null directory: PatchExeFS always put
the SD mod root into its patch dirs and then named every entry; ApplyLayeredFS returned early only
when both roots were missing; and the BIS and SDMC factories passed a null root on. NUP titles
reach ApplyLayeredFS at every boot (zortos293#203). Each missing root is skipped now. The new test, with no
filesystem factories, crashed before.

Port of Zortos zortos293#686, upstream Eglantine 34a791d (zortos293#728) by SmileyTatsu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With delayed fence operations (accurate GPU with default fences, or balanced or accurate fence
behaviour), a semaphore release without a fence waits in FenceManager::SyncOperation for the next
fence. A channel whose acquire missed released the fences once and suspended; CanResumeAcquire
then only re-read the semaphore, so a release queued after that waited for a fence nobody made
once every channel was suspended, and the GPU stopped with "waits on semaphore". The recheck now
releases the fences again, at most once a millisecond, while fence operations are delayed. Default
fences at lower accuracy are unchanged. No test reaches a suspended channel without a System.

Port of Zortos zortos293#687 (fixes zortos293#540), upstream Eglantine 90ff431 by kraibse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te mode

With GPU accuracy Accurate (no split), CallMethodSlow asserted unless every write during a macro
call went to executing_macro + 1, but NVN writes increasing runs over several macro registers and
all of them are the macro's parameters (ProcessMacro appends them, as the split front does): only
a write to an ordinary register while a macro collects is reported now. And MacroEngine::Execute
re-read native macros' parameters from guest memory after the pusher had fetched them, which
could only pick up memory reused since; native MME now runs the fetched words, as FrontRunMacro
does. HLE and interpreter macros keep their refresh. Fast and split modes are unchanged.

Port of Zortos zortos293#688 (fixes zortos293#538, refs zortos293#183, zortos293#598).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d CPUs

On Linux, LinuxProcessors() classified cores by cpu_capacity, which hybrid Intel CPUs report as
1024 for P- and E-cores alike (an i7-12700K or i7-12700H), so every core counted as a performance
core and the GPU thread pinning of zortos293#530 could put the GPU threads or the guest on E-cores. It now
reads the kernel's P-core list (/sys/bus/event_source/devices/cpu_core/cpus) where there is one,
parsed by Common::ParseCpuList with a test, and keeps cpu_capacity elsewhere. Pinning stays off
by default.

Port of Zortos zortos293#693, whose author measured pinning on against off in Ocarina of Time at 1080p on
an i7-12700K with RADV: 50.86 against 48.79 fps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The matrix lambda's parameter shadowed the function's own columns parameter, which GCC 16 reports
under -Wshadow; ENABLE_WERROR is on by default, so the build failed there (Zortos issue zortos293#674).
Renamed, with no change in what is emitted.

Port of Zortos zortos293#681.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The logging code calls format_string::get, which fmt has only from version 10, but a system fmt 8
or 9 was accepted (Ubuntu 22.04 ships one), so the build failed instead of taking the bundled
fmt 12. The minimum is 10 now, and older system copies fall back to the bundled one.

Port of Zortos zortos293#682 (fixes zortos293#668).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for it

With the hybrid P-core detection of the previous commit, pin_gpu_threads puts the split GPU
thread's front and back on a performance core each and the guest on the other performance cores.
In Ocarina of Time on an i7-12700H with an RTX 3050 Laptop, alternating runs with it off and on:
24.4 and 25.3 against 26.9 and 26.1 fps in Kokiri Forest at half resolution, and 34.8 against 36.8
fps in Link's house at native resolution. Presets now turn it on where Common::CanPinGpuThreads
says the host has the cores (six physical performance cores on Windows or Linux) and off
elsewhere; the detected host profile carries it, and the Performance page says so (Spanish
translation updated). Tested in performance_presets.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A decoder output image (gsv 911) is written pitch-linear until the client first asks for it with
gsv 428/426, and block-linear RGBA after. Nintendo Switch 2 Welcome Tour draws its "How Rumble
Works" slide once, from its clip's first frame, decoded before that first query, so the GPU read a
pitch-linear (and for code 5 BGRA) frame as block-linear and the slide came out sheared. The image
now remembers it holds a pitch-linear frame, and the first 428/426 rewrites it in place as
block-linear RGBA, swapping channels if needed, and invalidates the GPU's copy of the region.

Port of Zortos zortos293#689, a cherry-pick of 42fd03b23e by Jonas (jojo-chaechae) from Eglantine zortos293#699.
Not tested here: nothing on this machine runs Welcome Tour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a firmware installed, Mario Tennis Fever drew its controller button icons as "?": Switch 2
titles look them up in the standard font (type 0), and the firmware's standard font has no
private-use glyphs; they are in the extension font (type 5). Service::NS::MergeGlyphs copies the
24 button glyphs into a copy of the standard font, which the Switch 2 font commands serve as type
0; when the fonts can't be merged, or the standard font already has them (the synthesized
no-firmware font), the firmware's font is served as before. Bounds-checked table reads, with
tests in font_merge.cpp.

Port of Zortos zortos293#691, a cherry-pick of 34bf6746b3 by ymatrix1 (Yuri Olive) from Eglantine zortos293#534.
Not tested in game here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… out

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uery walks early

InvalidationAccumulator::Add took the range size before aligning the
address down to its 32-byte block, so a write starting past its block's
start got a range ending short of it: a lone word at 0x54 invalidated
[0x40, 0x4c), missing the word itself. Inline uploads and constant buffer
data written from mid-block (WriteWordsCached, WriteBlockCached) left the
rest of the first block uninvalidated, so storage buffers, textures and
shaders over those words could keep stale data. The bug dates back to
yuzu.

QueryCacheBase::IterateCache tested std::invoke_result (the trait type)
instead of invoke_result_t, so callbacks returning bool never ended the
walk early. IsRegionGpuModified now stops at the first dirty query;
SemiFlushRegion's callback no longer returns a value, so it still writes
every final value.

Ported from Zortos PR 700, which cherry-picks ProphetWild/ocarina
b3ce9f1 (a port of nemyu 99c953389 and Orchid d38d025) without its
experiment switch, and adds the tests. On the old code the first one gets a
12-byte range for the word at 0x1054 instead of its 32-byte block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EDEN_NX2_GPU_PROFILE, EDEN_NX2_CMDPAGE_DIAG, EDEN_NX2_DOWNLOAD_TRACE,
EDEN_NX2_PB_CHECK, EDEN_NX2_3D_METHODS and EDEN_NX2_SEM_WATCH were read
by namespace-scope initializers, which run before main() and so before
the frontend puts nx2_switches.txt into the environment. Set in that file,
the only way for a GUI launch, they did nothing.

The profile, command page and download trace flags stay plain globals,
since Enabled() runs several times per method write, and
VideoCore::CreateGPU reads them again before any GPU thread starts. The
DmaPusher and Maxwell3D switches become const members read when the
channel is made. These six were the only namespace-scope switch reads.

Ported from Zortos PR 714 (Segmentree), with a test that a switch set
after startup takes effect when read again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deferred clear begins its framebuffer's render pass with a variant whose
load ops clear. RequestRenderpass compared the framebuffer's own pass with
that variant, which never match, so the next draw ended the pass and began
it again loading: on a tile-based GPU that stored the attachments just
cleared and loaded them back, and the multisampled contents a variant
discards (only tilers discard them) were lost after the first draw. On
tilers the draws now stay in the variant, which is compatible since it
differs only in load and store ops. A pass continues only while one is
open, as ending a pass leaves the framebuffer it had in the state.

Immediate-mode GPUs keep restarting. With draws continuing on every GPU,
Ocarina of Time on the RTX 3050 Laptop restarted passes from draws about
135 times a second instead of 390, but ran at 35.0 against 35.5 fps in
Link's house (nine alternating runs, slower in seven of them) and the
same in Kokiri Forest.

Ported from Zortos PR 699 (vouion/oot-test-stack-integration 19c5ff0
by Gavin Schiller, with Zortos's fix for an ended pass), limited to
tilers, with the decision in render_pass_continuation.h and tested.
Nothing here is a tiler, so nothing changes on this machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cores

On a hybrid CPU with fewer than 8 performance logical processors, such as
an i5-1235U (2 P-cores, 4 logical), the hybrid placement kept about ten
time-critical threads (six guest cores, GPU front and back, Vulkan worker,
timing, fencing) on those few processors. Its author measured Ocarina of
Time in Kokiri Forest at 9.3 fps that way and at 11.5 fps with placement
off (EDEN_HYBRID_CORES=0). Placement now needs 8 or more performance
logical processors; below that the threads are left to Windows and the
log says so.

Ported from Zortos PR 698 (AxelBcr/oot-next fb0b747), with the
decision moved to Common::UsesHybridPlacement so it is tested on every
platform. Only Windows uses it, and nothing here runs Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 2026 Steam Controller stops sending sensor reports while it stays
connected: after it reconnects, and when Steam Input changes its sensor
mode. UpdateMotion restarted sensors only when reports arrived all zero,
not when they stopped, so gyro and accelerometer stayed dead until the
emulator restarted. The issue also reports that motion mapping ("Shake!")
never detects the controller, probably from the same silence.

SDLDriver::PumpEvents now restarts the sensors of a connected controller
with motion that has been silent for a second, backing off to eight
seconds while it stays silent and back to one once reports arrive
(MotionStallWatch, tested). The restart runs under the joystick's lock,
outside the joystick map's.

From Zortos PR 712 (emiliopedrollo, fixes Zortos issue 710), whose author
tested it with the controller on Linux. Its other half, comparing motion
with the first value since mapping began, is left out: while mapping,
InputEngine::SetMotion already keeps the stored value from before mapping
began, so the comparison already has a fixed baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zortos pull requests 715, 712's first half, 635, 638 and 621, and
Eglantine 292a68d, de4ed9d and 900ab5a, each with why it is not
here. Also notes that Zortos issue 619 (a GPU hang on NVIDIA driver 580)
did not happen in 13 minutes on driver 595.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under FMZ a zero times anything is zero, Inf and NaN included, so the result is the addend. The
translators left the zero select out when SAT was set, "as it does the logic for us". That holds
for FMUL, which has no addend, but FFMA turned 0 * Inf + 0.75 into NaN and SAT into 0, where the
hardware gives 0.75 (Zortos issue 745, found by reading the code; no frame is known to show it).
The select now runs with SAT too, and SAT clamps its result.

The half-precision path could not take that select. HFMA2 and HMUL2 compared F16 halves with an
F32 zero, and IREmitter::Select had no F16 case, so HFMA2.FMZ on half pairs threw a type error
unless SAT skipped the select (and HFMA2_32I.FMZ always did). The zero now has the operands' type,
Select emits SelectF16, and the pass that lowers F16 to F32 on devices without float16 lowers it
too. Native Switch 2 HFMA2 with FMZ lowers to these.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…limit

The mesh fallback binds its output buffer, and for task draws the payload, offset and argument
buffers, into the graphics pipeline's descriptor set. The per-stage check counts them, but the
set-wide check counted only the guest's storage buffers. On a device whose
maxDescriptorSetStorageBuffers sits close to a pipeline's count, it could create a layout over the
limit. FitsStorageBufferSetLimit now includes them and refuses the pipeline instead, without
overflowing the sum.

Port of Zortos zortos293#642 by RomainROCH, with its tests. Not run on a device near the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nsion

vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR was called on the selected GPU even when it
lacks VK_KHR_cooperative_matrix. The instance exposes the entry point when any driver has it (on a
laptop with an integrated and a discrete GPU, for one), and calling it for a device without the
extension is invalid. The outcome is unchanged: HMMA already needed the extension.

Port of Zortos zortos293#614 by RomainROCH. No test: the decision is one condition inside device creation,
which needs Vulkan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nx2:aud:u, the Switch 2 audio entry point, made a new IAudioInManager, IAudioOutManager or
IAudioRendererManager at every call. Each owns its own AudioCore manager, and AudioManager keeps
only the first buffer-release callback registered, so audio out sessions opened through a second
manager never got their buffers back: the title queued three silent buffers and its audio thread
stalled (Zortos issue 539). It now returns the audin:u, audout:u and audren:u instances, which run
on the same server, and logs an error if one is missing.

Port of the first half of Zortos zortos293#600 by GoatTech-42, from the reporter's fix. Its no-op for
IAudioRenderer command 14 is left out: unknown commands already get the automatic stub reply.
Ocarina of Time only asks for the renderer manager, once; it boots and plays here (null audio
output). No unit test: the services need Core::System.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e::state

The local `state` hides the member of the same name, and cl.exe builds stop on it with /we4458
(set only for MSVC without clang-cl, so the release builds compile). Renamed to `bypass`, after
its type; no change in behaviour.

Port of Zortos zortos293#649 by jpkto.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… slot

BuildNacp wrote the localized titles into their language slots, then copied the default title
into slot 0, the en-us slot, over an en-us localized title. An exact language match now wins: the
default title fills slot 0 only when no en-us title is there. Ocarina of Time's .nup has only a
default title, so it is unchanged.

Port of the first half of Zortos zortos293#738 by Kabosune (Eglantine zortos293#660). The second half, the game
list showing the en-us title, would hide a title edited in the .nup metadata here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Super Mario Bros. Wonder's Switch 2 Edition crashed at startup: it calls IPlatformServiceManager
command 2100, RequestLoadNxFont, with transfer memory for the fonts, and reads where each font is
from the nx2:pl:font session it gets back. The fonts are copied there at the offsets
GetBuiltInFontData uses, with the same standard font: the one that has the controller button
icons, which both commands now copy through Impl::WriteBuiltInFonts.

Port of Eglantine zortos293#646 by Alleexxi, tested there on Windows ("no more startup crash"). Nothing here
runs that game. No unit test: the command needs kernel transfer memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… collecting

The collector's critical threshold was capped at 5 GiB on every GPU so that Mario Kart World stops
filling 8 GB cards. On a 16 GB card its races then sit at that cap, and the collector evicts and
re-creates about 4,000 render targets every 600 frames, uploading them from guest memory. The cap
is now the larger of 5 GiB and 5/8 of the driver's budget: unchanged up to about 8 GB cards and on
integrated GPUs, whose budget is capped at 4-6 GiB.

Port of Zortos zortos293#268, with its author's measurement on a 16 GB card (53 -> 59 fps in Mario Kart World at 10 GiB). Not run on a large card here (this machine has 4 GB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A texture descriptor can ask for more mip levels than its size allows: Tomodachi Life: Living the
Dream asked for 15 levels of a 48x96 image, which has 7. The image was created with 15, which
Vulkan does not allow, and the device was lost leaving the characters menu or changing scenes.
Host images, views, uploads, copies and the accelerated ASTC passes now stop at the full chain
(HostMipLevelCount), and writeback skips the levels the host image lacks. The guest's mip offsets
and layer strides are unchanged, and an image that downscaling would cut short of its chain is not
downscaled. A warning names the first eight images limited this way.

Port of Zortos zortos293#575, with its tests (host_mip_limits.cpp). Its tester ran
Tomodachi Life for 40 minutes without the device loss. Ocarina of Time here logs no limited image
and plays as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No build had a translated interface: ENABLE_QT_TRANSLATION defaulted to OFF and no workflow set it,
so the 27 translations in dist/languages were never compiled into the executable (Zortos issue
743). Choosing a language then failed to load and LoadTranslation rewrote the setting to "en", so
the choice did not even survive a restart. The option is now ON. Without Qt's LinguistTools the
build goes on without translations and says so. LoadTranslation keeps a language it cannot load
and logs it.

Checked in the Docker build: with the option on, the 27 translations and the English plurals
build into lichii's resources (28 .qm files). A build directory configured before keeps OFF in
its cache. The interface itself was not run: this host has no Qt 6.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
javocsoft and others added 6 commits November 8, 2026 07:37
The NCE interpreter, which emulates memory instructions that fault on alignment, indexed the
signal context's 31 general registers with the register number. For a data register (Rt, Rt2, Rm)
31 is XZR/WZR, so index 31 ran past x30 onto the saved SP: `str wzr` stored the stack pointer and
`ldr xzr` overwrote it. GetReg now returns 0 and SetReg drops the value for register 31. Base
registers, where 31 means SP, already go through GetSp and SetSp.

Port of Eglantine zortos293#740, which found it in Paper Mario: The Thousand-Year Door hanging on a Mali-G720
Android device with NCE. Built only for arm64 hosts: nothing here compiles or runs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The state tracker points at the bound channel's Maxwell3D dirty flags, and every submission
invalidates state through that pointer. When the channel was released, or once the rasterizer was
being destroyed after its channels, the pointer outlived the flags, and shutdown wrote freed memory
(Intel Arc crashed on close). ReleaseChannel of the bound channel and the rasterizer's destructor
now point it back at the tracker's own flags.

Port of the all-vendor shutdown part of Eglantine zortos293#754. Its Intel-only workarounds and the
Mario Kart World task payload fix (written for Eglantine's mesh prologue) are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zortos pull requests 718, 721, 722, 727, 738's second half, 740, 741, 488 and 161, and Eglantine
f7a9027 and PRs 716, 717, 720, 721, 723, 739, 741, 754's Intel parts and 758, each with why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ocarina of Time issues per-object occlusion queries (its Z pre-pass and samples-passed queries)
and conditional renders on their results. The emulated GPU answers them slowly, and outdoors,
where the emulated CPU sets the pace, they cost about 3.4 fps. The new built-in patch turns both
render pipeline settings off where the game copies them into its nodes (six `ldrb w8, [x0]`
become `mov w8, wzr`), as cLohan's community OcclusionOff IPS does: the game stops issuing the
queries, and the depth test still hides what they culled. Like the other built-ins it applies
only to that build's main, only if all six words are the original ones, and it can be turned off
in Add-Ons.

Taken from Zortos zortos293#717 (stephenjayakar), without its environment switch. Kokiri Forest on an
RTX 3050 Laptop, two runs each, b29 (this with the kernel paths): 24.9 fps without it, 28.3 with
it. Screenshots of the forest and the porch match apart from the leaves and fireflies that move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Zortos zortos293#717 (stephenjayakar), without its environment switches and their old paths:

- KSpinLock spins for up to 20 us instead of 128 pauses before it sleeps.
- KAddressArbiter watches the word for up to 8 us before a thread sleeps in WaitIfLessThan or
  WaitIfEqual. If the word stops satisfying the wait, it ends with ResultInvalidState, which is
  what the kernel returns to a thread that entered the SVC that much later. A thread whose
  spins seldom pay off stops spinning.
- PhysicalCore guards its context with a KSpinLock, and Idle waits on the interrupt flag itself
  instead of a condition variable. Its 100 us idle poll pauses instead of yielding.
- KThread's context guard, which the scheduler only try_locks, is an atomic flag.
- The current thread and host thread ID are read from constinit thread_locals.
- GetParentReference computes each member offset once.
- SpinRecursiveMutex::try_lock reads before its compare-exchange.
- Memory traps on GPU-cached pages reuse the host pointer the access already looked up, keep the
  last four clean read areas per core instead of one, and remember a core's last host page to
  device page translation until the device mappings change (DeviceMemoryManager::
  MappingGeneration). Threads that are not guest cores get their own dirty memory slot instead of
  sharing, and locking, the last core's.

Not taken, measured on this tree: widening Ocarina's thread affinity (no difference). Kokiri Forest
on an RTX 3050 Laptop with Occlusion Off, two runs each: 27.9 fps without these, 29.4 with them.
New tests: kernel_locks.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its parts taken are in the two commits before this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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