152 Commits

Author SHA1 Message Date
chrg6
1e8045b36b Bring the tone mapped HDR picture's settings back to the menu, with defaults that give the look it had while it was fixed.
The sixteen settings the fixed look took away are settings again: the tone mapper choice, gamma correction, mid grey, match tone slope, tone slope, hue clip emulation, exposure, highlights, shadows, contrast, saturation, highlight saturation, blowout, flare, hue correction and colour grading. They have their rows in the settings table with the defaults and ranges they had, their rows in the Screen tab under HDR output's three brightness rows (none of them with the game's own HDR), their texts in the four languages, and their lines in the example ini. The tonemap library has the luminance mapper and the grading of the observer-response mapper again, and the support buffer carries five vec4 for it.

A player who changes nothing sees the picture as before. The observer-response mapper leaves each grading step out when its value is 1, and the slider values are divided, not multiplied by a rounded reciprocal, so 50 and 100 % arrive as exactly 1. Evaluated on the CPU in single precision, the shader with every default gave the same bits as the fixed one for 400,000 random scene colours, gains and whites, in HDR and for the SDR picture; the luminance mapper and the game's curve gave the same bits as before they were removed, and the observer-response mapper with its sliders moved stayed within 0.004 % of the pixel of its old result.

Gamma correction now counts with every tone mapper, 2.2 by default, which is what gave the fixed look its true blacks; before the fixed look it ran with the luminance mapper only and the other two showed the dark tones far above black. Its off value brings that lighter picture back for a player who asks for it. The game's own HDR does not take the setting and stays at 2.2.

Lines for these settings that an nx2navi.ini still holds from before the fixed look count again; the example ini and the release notes say so. Nothing of this was seen in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 08:35:47 -05:00
chrg6
7a8bbad47e Remove the low-end GPU mode from the settings.
The user asked for it: the mode no longer gains frame rate, and the mesh shader fallback covers the cards it was written for. Its switch in the settings menu's Game tab is gone with the rows under it, and so are their 57 text ids and strings.

low_end_gpu itself does nothing any more. It is still a known key, so an older nx2navi.ini gives no unknown-key warning, and while it is on the log says once that the mode was removed in 0.10.2. The preset that gave nine keys other defaults is gone, and so is the rule that read the mode's keys only while it was on: each of them (mesh_emulation, tex_clobber, foliage, vram_heap_limit, vram_reserve_mb, upload_batch_mb, mirror_stage, mirror_hot, submit_finish_gpu and the others) is an ordinary setting with the default of its row or of its reader, so a line a player wrote by hand keeps counting and a file with only the switch runs the game as it runs for everyone else. guest_opt in nx2navi.ini counts for the next rebuild without the switch too (tools/make_game.py).

The recorder thread read the switch itself: on an AMD card without mesh shaders the mode turned it on. It is off on every AMD card now unless vk_recorder = 1 is set.

nx2navi.ini.example lists the former mode's settings as single settings, the 0.10.2 notes have a Removed entry and name the foliage fix of the commit before this one, and the comments that said what the mode sets say so in the past tense. tests/cfg_parse_check has the new reading of the switch and of the keys without it; the test matrix is at 90 ok with the same two not ok as before (installer_test, worktree_test), and a clean build compiles 169 runtime files and links. Not run in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 06:13:18 -05:00
chrg6
6e57ef9619 Check three of this pass's decisions without a device: the automatic texture detail, the device made with DLSS Frame Generation on, and the mesh fallback's depth hold.
Each decision moves into a header the runtime and a test both read, as vk_mesh_path.h does: vk_tex_detail.h (tex_detail_auto), vk_dlssg_device.h (dlssg_card_skipped, dlssg_device_create) and mesh_fb_hold.h (the hold's table). The runtime's behaviour is the same as before the move.

tests/tex_detail_check has the budgets from players' logs of 0.10.1 (a handheld's integrated GPU at 10,361 MiB now gets reduced, a 12 GiB card at 11,313 MiB full). tests/dlssg_device_check plays the laptop of the report with stand-ins for the interposer and the loader: the chosen Intel card is skipped, the NVIDIA card is made through the interposer, a card of another vendor is made by the loader without the interposer being asked, and a refusal falls through to the loader. tests/mesh_fb_hold_check holds a list for two presents after its colour draw was skipped, through a wait of 600 frames, and no longer once it is drawn or gone. Built against the rule 0.10.1 had, the first two and mesh_path_check fail (6, 13 and 3 checks).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 05:35:49 -05:00
chrg6
bbeecce88c Never take the mesh emulation on an NVIDIA card, where it ended the game inside the driver.
The low-end GPU mode sets mesh_emulation = 1, and on a card without mesh shaders the emulation then draws the game's task and mesh programs in place of the mesh fallback. On NVIDIA's cards without mesh shaders that ended the game at the first mesh draws of an area with an access violation inside the driver, at nvoglv64.dll+0x10cdffe on a GTX 1050 Ti and on a GTX 1070 (both 0.10.1), and a Titan X (Pascal) report names the same mode. The GTX 1050 Ti's owner set mesh_emulation = 0 and played on through the fallback with the rest of the mode on.

The decision (vk_mesh_path.h) takes the card's vendor: an NVIDIA card is drawn for as if the emulation were not asked for, and the boot line says so. The replay and the pipeline code read the decided path from the device context where each read the setting itself. tests/mesh_path_check.c has the new cases.

Not run in the game: no GTX 10 card here. mesh_path_check passes and every file that includes the changed headers compiles.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 05:27:59 -05:00
chrg6
787b7c73de Leave a patch of grass out whole while its colour draw waits for a pipeline on the mesh shader fallback.
The game draws a culled list of grass or leaves twice from one indirect record: into the depth target alone, then into the colour targets. On the mesh shader fallback each draw has compute modules and a pipeline of its own, made at its first draw, and the depth-only one is ready first. Drawn alone it left the list's depth in the scene with nothing drawn over it, so what lies behind failed the depth test there and the sky's colour showed through the ground in the shape of the grass for as long as the colour draw's pipeline built, which is seconds each on AMD's driver for Radeon RX 6000 and older, where the fallback is the default.

A colour draw skipped for its modules or pipeline now stamps its record's address with the present, and clears the stamp when it is drawn. The depth-only draw of a record stamped within the last two presents is skipped as well, counted like a draw whose own pipeline is building. A draw whose stages write memory is never held. mesh_fallback_hold = 0 draws the depth alone as before.

Not run: no card here takes the fallback by default, and the game is the user's to start. The changed file compiles and the settings table's check reports 0 problems.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 04:35:10 -05:00
chrg6
ddc0247b25 Count the draws skipped for a pending pipeline with atomics, as the replay adds to the count on one thread while the present takes it on another.
pending_draws (vk_exec_draw.c) counts the draws a frame left out because their shader or pipeline was still building;
vk_backend.c frame_hold takes it at the present to hold such a frame back. The comment said both happen on the replay
thread, which is true only with the replay on its own thread. With the replay on the submitting threads, draw_skipped
adds on the thread holding the submission lock (and its parallel chunks) while vk_exec_pending_draws_take reads and
clears it on the presenting thread, so a plain ++ and a read-then-clear could lose a count: a frame missing draws shown
instead of held, or the next frame held for nothing. The add, the take (an exchange) and the read that skips a link
wait are atomics now.

Not reproduced before the change. Checks that passed: pass_cc_check of vk_exec_draw.c with no new -Wall -Wextra
warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:13:02 -05:00
chrg6
2f640fb8ed Make the stutter map count runs of long frames as one hitch and the texture pool's area loads as loads.
In the 2026-11-13 session three frames of 87, 91 and 96 ms after an area load each counted as a change of pace, as each was followed by one as long, and loads that only the texture pool saw (no area file read, so no scene line) were charged to play. Consecutive long frames are now one episode, counted once with their summed time, its line giving that time and the first frame's parts; a run of more than 8 long frames none of which stands out is still a change of pace. The texture pool's start and end of an area load mark an area load for the map, as the game's own loads do.

With trace = perf, the slow write-watch queries and freezes past the 200 lines they may log are counted and given every 60 frames, so a late area load still shows them; the session's lines had run out by frame 36,387.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 15:57:52 -05:00
chrg6
edd5d04e8c Cut a mesh fallback draw's workgroup count that the GPU wrote to what the fallback's buffers hold before a dispatch reads it.
The record's copies put the count the game's culling pass wrote into the first dispatch and, without a task stage, into the draw's instances as it was. A garbage count launched up to millions of workgroups that write nothing and instances that draw nothing, enough to keep the GPU past the driver's timeout; with the fallback now the default on AMD's driver for RDNA 1 and 2, such a count would have moved a hang there instead of removing it. A single-thread compute pass (shaders/meshfb_clamp.comp) cuts the count to the buffers' room, which changes nothing that is drawn; the report word keeps the count asked for, so record_check still logs a mesh-only draw that asked for more than the buffers hold. On for every device that uses the fallback: one dispatch and one barrier per indirect fallback draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:54:24 -05:00
chrg6
1f4818fb6b Name in a lost device's report the mesh draws of each marked pass, a group of draws the replay workers recorded between two marks, and in the fatal line how mesh draws are drawn.
A pass mark now counts its mesh draws and those that take their count from the GPU; the gap before a mark notes a group recorded by the workers, whose passes have no marks, where the report said "barriers and state only"; the fatal line names the mesh path (native, the mesh fallback, the emulation). The comment on the RX 6700 XT report in vk_exec.c called the last marked dispatch the replay's own: it is the game's (op_dispatch, cs d7566eb1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:54:24 -05:00
chrg6
5faa1687f4 Add quad_index, which draws an indexed quad list as two triangles per quad and closes an indexed line loop, from the game's own indices, and log the first such draw either way.
Vulkan has no quad or line-loop topology. Draws without indices were rewritten already; an indexed quad list went to the GPU with its indices under the triangle list topology, three at a time, joining corners of neighbouring quads, and an indexed loop lacked its closing line. Off by default and read at start; no log of Ocarina has shown such a draw so far, and the new line says when one comes. tests/index_rewrite_check.py checks the index lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:36:46 -05:00
chrg6
3573a96f46 Keep the SPIR-V of shaders nobody read for 30 seconds compressed in memory.
The shader memory cache held 777 MiB of SPIR-V for 7115 shaders for the whole session. A background thread now
compresses the SPIR-V of each shader no reader pinned for shader_compress_idle_s seconds (zstd level 1, 26% of the
bytes on the test install's 44,151 modules) and frees the plain copy; readers pin it through gfx_shader_spirv_pin and
get it decoded back. What the drawing thread compares at pipeline misses and draws, the module's FNV-1a and its Binding
decorations, is kept with the shader, so module_binding_missing, the water corrections and the DLSS, crumbs and skip
lists decode nothing. A compute pipeline created at a dispatch and a mesh shader's first pipeline key still decode on
that thread, and the memory report counts those decodes apart, with the SPIR-V as held against uncompressed and the
shaders held compressed. shader_compress = 0 keeps it plain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 07:55:56 -05:00
chrg6
97671e8ea7 Add the stutter map: each hitch of a play session with its area, what the frame waited for and the pipelines and shaders its draws met missing, and every 10 minutes and at exit the areas ranked by their hitches and the pipelines that cost the most (stutter_map = 1, off by default). tools/stutter_map.py turns a log into a report and counts what a game folder recorded that a pipeline list lacks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 07:31:57 -05:00
chrg6
d9429b0c00 Break the memory report down past VMA, by owner in system memory, and by growth over a session.
Every report adds a line for video memory outside VMA (the driver's usage and budget per heap against VMA's blocks, and counts of pipelines by kind and by last use, stage libraries, compute pipelines, layouts and descriptor pools, with the swapchain, DLSS and FSR sizes), a line that splits the process's commit by owner (the game's heap and maps, device-local memory, VMA's host blocks, the C runtime heap with the shader caches, pipeline tables, texture cache, log queue and flight recorder in it, the driver's pipeline cache data and the rest), and a line naming what grew by more than 64 MiB since the report before and since play began. Every ten minutes of play and at exit a ranking line orders the categories by growth and marks those that only climbed; tests/mem_growth_check.py checks that bookkeeping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:46:31 -05:00
chrg6
58c7baaa9e Bring frame generation without an upscaler from the 0.10.0 line into the VRAM pass.
The release of what the upscaler leaves now waits for frame generation's own inputs too: in that mode the DLSS sizes
are made for frame generation alone while the upscaler does not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:52:36 -05:00
chrg6
e0f69489af Bring the 0.10.0 line into the VRAM pass, keeping the line's own releases where the two overlap.
The line freed the upscaler stand-in's scratch images, FSR 1's and SMAA's images and the DLSS sizes on its own while
this pass did: its versions stay. Of this pass's versions, what they do not cover stays: the stand-in's views of a game
image it read as it is now go with that image (vk_tonemap_forget from vk_exec_image_gone, under a lock), and the
upscaler release frees only what the line's idle release leaves, the reactive mask's snapshots, the FSR upscale
contexts and the DLSS sizes after a failure, at the same 600 presents and with vram_use = high keeping them as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:50:21 -05:00
chrg6
e89cc4d430 Let frame generation run without a temporal upscaler, from inputs made for it alone.
With frame generation running and no DLSS or FSR upscaling, vk_dlaa.c arms and ends the frame in the same places and,
where the upscaler's evaluation would run, makes only frame generation's inputs (fg_only_frame): the scene's depth and
camera found as before, motion vectors from the same pass at the scene's size, no jitter at all, and the picture without
the HUD copied from the game's own scaled picture. DLSS Frame Generation and AMD's frame generation no longer need an
upscaler set; NVIDIA's through NGX still needs DLSS at the start and then keeps running when the upscaler is switched
off. The menu's Frame generation row stays usable with every Upscaling choice and the Upscaling row keeps frame
generation as set, with texts in all four menu languages. The log says at the start and at each change whether frame
generation takes its inputs from the upscaler or runs without one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:49:30 -05:00
chrg6
ad58dfcddb Keep the fallback blit's scratch images for the sizes in use, and let the tonemap pass's views go with their image.
The scratch images of the fallback blit, one per size and format, were kept for the process, the first eight ever
asked for. Their sizes follow the scene's resolution, which DLSS modes, the dynamic resolution and a window or
picture size change move, at up to 32 MiB each at 2160 lines; a ninth size got no scratch and its blit was left out.
A new size now takes the place of the one used longest ago, which goes once the GPU is past the submission being
recorded, and never one the submission being recorded uses. The cache is under a lock, since replay workers record
blits side by side.

The tonemap pass's views were kept by image handle for the process, a game image's view naming a destroyed image that
a new image could get the handle of, and no view at all past sixteen. A view now goes with its image: at a scratch
image's replacement, and with a game image through the replay's image-gone hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:41:47 -05:00
chrg6
f2849c8b26 Merge main's tail_early, replay_small and Testing rows into the 0.10.0 line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	runtime/cfg.c
#	runtime/vk_exec_xfer.c
2026-11-13 00:26:53 -05:00
chrg6
a4310491de Let the upscaler stand-in's scratch images make room for a new size instead of refusing it.
The cache kept the first eight sizes and formats for the whole run and refused every other one, so after a few
window, resolution or DLSS mode changes the stand-in's blit of a new size was left out, while the old sizes' images
stayed. A new size now takes a free entry or the one used longest ago, whose image goes through the deferred
destruction after the tonemap pass drops its views of it (vk_tonemap_forget), before another image can take its
handle. tests/scratch_lru_check.py checks the choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 23:21:17 -05:00
chrg6
2571e180c8 Check the texture pool's set against the device's descriptor limits, and name the slot a carried translation reads.
The larger pool asks for 655,360 sampled images in one set where 524,288 were asked before. The start now logs the
device's limits for one set and one stage next to that count, warns when the count is over them, and a refused
layout ends the start with the limits named instead of a bare failure.

The log line for a slot registered past the pool names both slots a shader reads instead: the pool's last entry, and
the last of the old length for a translation carried over from the translator before. The comments say what they say
without an issue number.

The translator's identity changes with this pool size: after the update every install translates its shaders again
in the background, the earlier translations serve meanwhile, and a shader gets the ground fix once it is translated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 22:46:24 -05:00
Leyukaka
2388f1f581 Give the texture pool room for Ocarina's 33068 descriptors instead of 32768.
The game makes a texture pool of 33068 descriptors, and the translator clamped every pool handle to 32767, so the textures registered from 32768 up were drawn with whatever slot 32767 held, often an empty image: ground went see-through after an area load until the area was loaded again. VK_POOL_TEX and the translator's TexturePoolLength go to 40960, and a slot registered past it is logged once.
2026-11-12 22:45:06 -05:00
chrg6
89ed3ffd17 Read ahead for mesh draws too, and let a mesh draw wait for its fast link like a vertex draw.
The look-ahead of a submission passed over mesh draws, as their pipelines had no stage libraries. Where they have
them now, a mesh draw the scan reaches is looked up with its mesh and task stages and its state is named to the
pipeline hints, so the library of its task and mesh stages, the one that takes longest to make, is building before
the replay gets to the draw. Never with the mesh fallback, which draws through vertex pipelines of its own.

A mesh draw that waits for a fast link was cut off once its pipeline had been building for 8 ms, since no link
could come. That rule now holds only for a mesh pipeline without stage libraries: with them the draw waits as a
vertex draw does, the few milliseconds of pipe_link_draw_ms from its pipeline's first miss. A one-shot mesh draw
keeps its 8 ms: when the caches lack the task and mesh library, making it is as long a compile as the whole pipeline
was, and the frame's wait pool is not spent on that.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:31:13 -05:00
chrg6
a79b0a5125 Remove the counters of what a moved texture slot generation costs the draws' kept bindings.
Two play sessions answered the question they were added for: about 0.3 whole-draw skips a frame are lost to the generation moving alone, too little to build the per-slot check for.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:06:15 -05:00
chrg6
264dbe42cc Merge the stability and memory pass into the 0.10.0 line.
Conflicts: the settings key lists and section maps take both sides' entries.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 21:49:17 -05:00
chrg6
1f5ee67f7e Give every skinned character motion vectors of its own, not the player alone.
A scene draw whose vertex shader reads a matrix palette and whose vertex layout feeds its joint inputs unsigned integers is drawn a second time into the motion vectors with last frame's blocks and camera, as the player's meshes are, with the same depth test and the same guard for bones that turn sharply. Which draw of last frame it continues is decided when the vectors are made, so the replay's threads cannot change it; a draw in doubt keeps the camera's motion. The player's own draws stay with their ownership records.

skin_motion (NX2NAVI_SKIN_MOTION) is 1 by default and 0 turns it off. The motion status line now counts the skinned draws seen, those drawn with their own motion and the others by reason. It has compiled and passed its checks; it has not been played.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 20:03:59 -05:00
epicmeme546
aec5e72051 Let the game go on while the GPU runs the end of the last frame, with tail_early = 1.
The game waits each frame until the GPU has finished the frame before. In Hyrule Field with FSR 4 the last of that
work is one part of 4.7 ms, the temporal upscaler and the interface drawn after it, then 1.4 ms of write-backs, and
what the game reads of the frame is done before the upscaler runs. With NX2NAVI_TAIL_EARLY the replay records that
tail into a command buffer of its own, and when it hands the CPU nothing but timestamps, writes no pool memory but
inline uniform updates of mirrored blocks and reads no guest memory late, the write-backs go to the queue before it
with the executed timeline's signal. The game's waits end there, and the GPU runs the tail while the next frame is
prepared. The GPU's commands are the same; a tail that fails a test goes as before. Off by default.

RX 7900 XT, Ryzen 7 5800X3D, 3440x1440, FSR 4 Native AA, one batch, replay workers off: 50.7 against 42.5 fps
standing and 56.0 against 45.9 along the field route; with FSR 3.1, 51.8 against 47.8 and 54.9 against 52.3. With
three replay workers, FSR 4, standing: 63.5 against 49.4. The screenshots matched those without it standing, along
the route and through warps to five areas, with no black frame outside the loading screens of both.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 17:41:47 -05:00
chrg6
20ab8d0d2d Count what a move of the texture slot generation alone costs the draws' kept bindings.
Registrations no longer move the generation while a submission is replayed, but the replay's own lookups still do when they take a released texture back or make one, and so does a texture another thread of the game finalizes meanwhile. Every such move drops the whole-draw skip, the shader memo and the kept descriptors of a draw, buffer descriptors included, whether or not the slots the draw reads changed.

A new line in the texture pool's periodic log says, per frame, how many graphics binds found the generation moved within their command buffer, how many whole-draw skips and shader memo hits it alone cost, and how many kept buffer and texture descriptors were resolved again from the values they were kept under. One play session then shows whether checking only the slots a draw reads would spare anything. The counts are kept per recording context, since the replay workers record at once.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:45:12 -05:00
chrg6
95dc476ee7 Scale the scene with the edge-adaptive filter where the game's own upscaler ran.
The stand-in for the game's upscaler enlarged the scene with a linear filter, which is softer than the console's picture. It now enlarges with the edge-adaptive filter the present already scales to the window with: the same shader, built a second time with a constant set so it squares what it filtered, as the linear pass does. The stored square-root values are filtered as before and the decode is the same. standin_filter = bilinear brings the linear pass back, and the setting is live. The linear pass also stays where nothing is enlarged and while DLSS or FSR 3.1 runs, since that upscaler scales the scene itself and its picture goes over the stand-in's. The movies do not go through this pass and are not changed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:16:18 -05:00
chrg6
1c975316a3 Skip the index-buffer bind of a draw that repeats the one before.
Every indexed draw bound its index buffer. The command buffer's kept state now holds the last bound buffer, offset and index type, and a draw with the same three records no bind. They are cleared with the rest of that state wherever the replay continues in another command buffer or something else has bound into this one, as the kept vertex streams are, and the indices the replay makes itself for quads, line loops and byte indices go through the same bind.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:12:51 -05:00
chrg6
91dd6de969 Make a draw whose shaders write memory wait for its pipeline like a one-shot draw.
A draw into targets the game renders every frame is left out while its shaders or pipeline build, because the next frame draws it again. A storage buffer the shaders write or an image they store into is not redrawn that way: with the draw left out, that memory keeps what it held for everything that reads it. Such a draw now takes the wait a draw into one-shot targets takes, from the frame's wait pool, and is skipped only at the end of it. Only the stages already translated can say that they write; a draw whose writing stage is still being translated is skipped as before. draw_writers_wait = 0 turns the rule off, and the periodic line about waited draws counts the draws that waited by it alone.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:12:51 -05:00
chrg6
cacf458b08 Say in a lost device's report what the GPU was about to do when it stopped between two marks, and add draw marks.
A pass mark names the pass or dispatch the GPU had started and the last it finished. When the two are the same mark the GPU stopped in between, and the report named nothing: in a player's log of 0.9.2 (RX 6700 XT) both were one compute dispatch, with 51 later marks made and none started. The report now adds the mark that never started and the kinds of commands the replay recorded between the two (copies, clears, the upscaler, write-backs), which the gap notes already name for the GPU pass timing and now keep for every mark.

draw_crumbs = 1 (off by default, AMD buffer marks only) also writes a mark wherever the shaders or the pipeline state change inside a pass. The device losses reported on RX 6600 and RX 6800 class cards end inside one scene pass of 30 to 100 draws and the draw that hangs is not known; with the setting on, the report lists the shaders of the draws the GPU had not finished.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 14:57:21 -05:00
chrg6
26f3dc47ec Show the low-end GPU mode's settings under its switch in the Game tab, and read them only while the mode is on.
The settings had a tab of their own; they are rows of the Game tab now, listed under the switch once the mode is on after a restart, and the tab is gone. With the mode off every one of them reads as unset, whatever nx2navi.ini or the environment says, so nothing of the mode acts unless a player turned it on; the build reads guest_opt the same way. The notes describe that, and take the white edges in towns and the dropped textures from the known issues to the fixes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:59:43 -05:00
chrg6
e3cf0710c5 Record a draw's own texture binding that got the stand-in, and a sampled pool slot that is not registered.
In towns, kerb, stair and wall-top edges are drawn untextured for one frame when the game registers a round of streamed textures, and no pool entry of those textures goes to the stand-in in such a frame. A binding written into a draw's own set, and a slot that is unregistered when its marks are read, left no event until now.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:08:16 -05:00
chrg6
2324a74602 Turn the settings for older graphics cards on with one key, low_end_gpu = 1, and leave them off without it.
Mesh shaders as compute, the texture that gets memory of its own, staged mirror uploads and the shorter wait for the GPU, widened write-watch queries, the video memory limits and reserve, early upload batches, the asynchronous replay, the hot-pool rule and the recording thread on AMD cards without mesh shaders were measured on a Radeon RX 580 and run on no other card. Each keeps what 0.9.1 did unless the key or its own setting says otherwise; the key gives the defaults the way the device profile does. The notes describe them as that option, hold main's two new entries in one Changed section, and list the white edges in towns as a known issue.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:00:13 -05:00
chrg6
f2b6f39c5f Give a natively run network program this frame's input picture: the RGB9E5 words the post pass stored in the scene target are copied into the RGB9E5 texture the network samples.
The game's scene upscaler samples the post pass's picture through an RGB9E5 texture at the scene target's address. On the console both are one memory; here they are two images, and the RGB9E5 one kept what it held when it was made. With NX2NAVI_NATIVE_TENSOR=1 the network therefore read an old picture or the target's R11G11B10F words as shared-exponent texels, the source of the infinities and noise it wrote. The stand-in already read the target's image for this reason. A network program's output image without storage use is now logged.

Only dispatches of tensor-core programs take the new path, and those run only with NX2NAVI_NATIVE_TENSOR=1, which stays 0 by default. The file compiles without warnings; no game run has checked the picture.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:39:54 -05:00
FIocker
5e71cee08c Record the depth bias again at each pass, as the viewport and scissor are, so a pass into a scaled shadow map never keeps an unscaled slope term.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 12:39:53 -05:00
FIocker
58efccd883 Scale the slope-scaled depth bias with the shadow map scale, so the larger maps keep the bias the game asked for.
The slope term is a bias per unit of depth slope across one pixel of the target. A shadow map made k times larger on
each side (shadow_scale 2 to 4) has 1/k of the slope across each texel, so the game's factor alone gave k times less
bias on sloped surfaces than at the game's own size, which is what shows as acne. vk_exec_draw.c slope_bias
multiplies the factor by the pass's scale for both the direct draws (record_fixed) and the queued mesh draws; the
constant term counts the depth format's smallest steps and keeps its value. Nothing changes at shadow_scale 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 12:39:53 -05:00
chrg6
a464ffafbc Merge Tobim6's work for older graphics cards: mesh shaders emulated with compute, video memory kept within the heaps' budgets, staged mirror uploads and the submit wait that goes with them.
Conflicts: the settings key lists take both sides; frame generation keeps the camera-cut handling and logs its counters every 600 frames with the cuts among them; the pass-time mark moves into draw_viewport; mo_desc keeps the pose depth and notes the reader.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:14:58 -05:00
chrg6
f45273bfe5 Say in the flight recorder's blank-slot events what the slot resolves to, and whether any shader reads the pool as a volume.
Ten slots a frame are noted as having no picture when their submission ends: the 128x72 and 64x36 volumes the game registers under new slot numbers in every frame. The events now tell a slot whose entry cannot be viewed in any kind a shader reads from one whose lookup found nothing.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 11:28:22 -05:00
chrg6
b1e8c1427b Merge branch 'claude/pr-intake-6-43af31' into ra-all-test 2026-11-12 11:23:54 -05:00
chrg6
5f10a0012b Merge remote-tracking branch 'nx2navi/main' into ra-all-test 2026-11-12 11:23:49 -05:00
epicmeme546
322d7c0933 Run DLSS or FSR where the game's own upscaler runs, and fill a jittered pass's edge rows as soon as it ends.
With the HUD hidden the screen showed the scene at its render size instead of the upscaler's picture. The upscaler ran at the pass after the game's own upscaler, taken for the pass the HUD is drawn into. Without a HUD the game draws no such pass: the next one is the frame's last, which encodes the stand-in's scaled scene into the 8-bit output. The upscaler's picture went into that target and the last draw drew over it. A player's save with the HUD off showed blocks of render pixels, a picture jumping by a render pixel between frames, and the undrawn edge row in white, at every FSR mode and at Native AA; every test save until now had the HUD on. vk_dlaa_upscaler now runs the upscaler right after the stand-in of the game's upscaler wrote its output image, and puts the picture there (NX2NAVI_DLAA_AT_UPSCALER=0: as before).

The edge fill moves to the end of each pass that took the jitter (vk_dlaa_pass_end), for the pass's colour targets and depth, in one copy of eight regions per image. Filled only before the upscaler, the undrawn row had already been read by the water, the bloom and the anti-aliasing pass.

On a copy of the player's save (HUD off), FSR 4 Ultra Performance, world slowed to 1/20, present screenshots 0.25 s apart: the picture moves 0.00 to 0.02 px between shots (old placement: a jump of one render pixel in 5 of 20), the change on edges is 1.9 against 16.9, and no shot has the white row (half had it, with HDR on). Native AA, Performance with HDR, and FSR 3.1 are steady and clean as well; the save with the HUD on is unchanged. A run through a stream: 0 of 106 shots with a bright bottom edge (20 of 102). Frame rate 58.3 fps against 58.6.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 11:14:16 -05:00
chrg6
a389f2450f Add a switch that compares textures over 64 KiB by content before uploading them again, and hash eight chains at once.
While the player runs through a town the game moves about 32 textures to new storage every four or five frames, back to back in one block, and a frame after their first upload every page of the block shows a write again. A texture up to 64 KiB keeps a hash of its storage and is uploaded again only when its bytes changed: of those, 0 to 4 in a hundred are (9 of 440 in one recording of 600 frames). A larger one has no hash and 75 to 94 in a hundred are uploaded again, whatever the size and format: 3,034 second uploads in those 600 frames beside 4,233 first ones, the ASTC ones decoded and encoded on the GPU a second time. The frames of the second uploads take 0.9 to 2.5 ms longer than frames without uploads, and the frames after them wait 0.3 to 1.3 ms longer for the GPU (six recordings of one player).

tex_same_big = 1 has textures up to 8 MiB keep the hash too. It is off by default: the bytes are read once more at the first upload and once at the comparison, 15 us each for 300 KiB, which together is about what the second upload takes on the submitting thread (27 us), so what it gains is the GPU's work and whatever else a second upload costs its frame, and that is not measured in play. A texture whose comparisons found other bytes twice in a row takes no hash any more, and an area load takes none.

The hash runs eight chains side by side and folds them, 20 GB/s against 7 for the one chain on a Ryzen 7 9850X3D, which the small textures' comparisons get as well. Each step is still a bijection of its chain and of the word it takes.

The pool log's buckets line counts the hashed uploads and the comparisons when there are any. --probe-vkmem has a case that passes with the switch off and on: the same bytes written again, other bytes, and the same bytes after two rewrites in a row.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 10:49:54 -05:00
automata.
b25fba2e55 Add shared pose motion vectors for Link and attached equipment 2026-11-12 10:42:35 -05:00
DeepDaddyTTV
5b7703e08e Record the draws rejected during preparation in a captured frame.
A draw can be left out before it opens a render pass, and the pass list of a frame capture then showed nothing of it.
passes.txt now ends with the rejected draws of the captured frame by mesh or vertex draw and reason, each with the
first program it met, and says so when there were none. A draw left out by NX2NAVI_SKIP_PROG or NX2NAVI_SKIP_SHADERS
is counted under a reason of its own. The frame to capture and its files are set together under the recorder's lock.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 10:40:53 -05:00
Tobim6
922e364f50 Staged mirror uploads: device address table readers noted at every bind
gpl noted them in ssbo_info, which the bind skip and the kept descriptors leave out once they have warmed up: the
submissions were taken as reading no guest memory again, and the grass and moss flickered again after a minute of play
(every submit wait of a 2100-frame run ended without the GPU). gpa_reader looks once per pipeline for a table binding
and notes the submission at each bind_resources and mo_desc.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:23:21 +01:00
Tobim6
f23f413202 Staged mirror uploads: a submission that reads through the device address table waits for the GPU
The table names pools without a mirror, guest memory the GPU reads as it runs, and nothing marked such a submission
live: with NX2NAVI_MIRROR_STAGE the submit wait ended once the replay had submitted, and the game wrote the next frame's
foliage instances while the GPU still drew from them (small grass and moss flickering and sliding as the camera turned).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:23:20 +01:00
Tobim6
15744c688a Texture pool refresh: the slots a refresh left are kept in a list, and the next refresh of the submission looks up those and the changed slots instead of walking the whole table again.
While an area loads, every refresh left slots, and with any left the next one walked the table: about 67,000 slot reads
and 1.65 ms of the replay per submission in Kokiri Forest on an RX 580, six submissions a frame (profile 2026-11-11:
pool_fill and the slot lookups under the first pool draws, mesh_emu_draw's among them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:23:09 +01:00
Tobim6
306de50589 NX2NAVI_FOLIAGE (full, reduced, off): reduced skips the indirect mesh draws with a task stage (small grass and moss), off every indirect mesh draw (all foliage); a Lama patches row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:23:05 +01:00
Tobim6
ff1c1ed87a Mesh emulation: the stages of a pass's emulated draws recorded by stage, with one barrier between stages for all of them.
Each draw's indices are now a range of its own and its task records too, reserved by an allocation shader (one
invocation, mesh_emu_alloc_spirv) for the run of draw records queued: before the task stages the task records and the
index ranges of draws without a task stage, after them the index ranges of the others (mesh workgroups with output,
which the task stage now counts in the record, times OutputPrimitives x vertices per primitive). The mesh stage takes
its indices from the draw's range with an atomic in its record; vertex slots stay the arena's, as the indices name
them. So no draw's stages need another's, and the queue (MEQ) records the fills as the draws come, then at the
pre-pass command buffer's end (vk_sync.c pre_end -> vk_exec_mesh_flush): allocation, every task stage, allocation,
every mesh stage, a barrier between each. One by one each draw took three barriers and copied the arena's counters
after the draw before it: an RX 580 spent 5.5 ms of a frame's GPU time on 69 draws' stages in Kokiri Forest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:23:02 +01:00