Skip to content

Vulkan backend with TAA and GTAO - #110

Draft
NightHammer1000 wants to merge 293 commits into
StratumServer:mainfrom
KillerPixelCrew:feat/vulkan-taa
Draft

NightHammer1000 wants to merge 293 commits into
StratumServer:mainfrom
KillerPixelCrew:feat/vulkan-taa

Conversation

@NightHammer1000

@NightHammer1000 NightHammer1000 commented Sep 17, 2026

Copy link
Copy Markdown

Follow-up to #69, reduced to the Vulkan backend and TAA as agreed there. Upscalers and frame generation are not in this branch.

The backend replaces the platform class rather than branching the GL calls: VulkanClientPlatform overrides the graphics members of ClientPlatformWindows, so OpenGL keeps running the unmodified base and stays the default. Vulkan is opt-in through Renderer in optimum.json.

Still experimental. I run it daily on an RTX 4070 (Linux, driver 615); nothing here has been verified on AMD or Intel yet.

Done

  • Vulkan 1.3 backend: explicit synchronisation on timeline semaphores, asynchronous uploads, presentation decoupled from the frame, a streaming frame graph that derives barriers from usage, pipeline and shader caches on disk
  • The 49 vanilla shader programs as native GLSL 450, compiled offline to SPIR-V with a manifest; mod shaders still go through the runtime rewriter
  • Every draw of the default configuration goes through the native device API: terrain, entities, particles, decals, sky and clouds, GUI and text, the post chain
  • TAA: jittered projection, motion vectors for all geometry classes, history resolve with nearest-depth disocclusion and anti-flicker weighting, RCAS-style sharpen with noise limiting
  • Ambient occlusion: a GTAO port (visibility bitmask) composed before the resolve, selectable next to the game's SSAO
  • Headless capture harness (real client, no visible window, frames and attachments to disk) and a GPU test suite that runs with synchronisation and best-practice validation on
  • From the Adding Vulkan, TAA, Upscalers and Framegen #69 review: libshaderc_shared.so lands in the application root, prime-run is only used when present, python3-numpy is in the prerequisite check, teardown drains the frame and transfer timelines before the device goes, the donor-drift and swapchain tests are green here, and there is no XeSS surface on this branch
  • Assistant notes and plan files are ignored and untracked; docs, comments and tests no longer reference them
  • The GL-shaped emulation layer is gone: no state tracker, no texture-unit tables, no uniform lookup by name. The platform records what the client states and every draw goes through one generic native path
  • Frame identity and markers: one latency frame id per frame from a pre-input sleep seam, markers around simulation, render submit and present, every submit of a frame tagged, one present id per present chained as VkPresentIdKHR where the device offers it, and a stats.latency line with the frame's breakdown. The vendor latency backends are not in this branch, so nothing paces the frame differently
  • The world frame and the UI are separate images: the scene is rendered HUD-less, the GUI draws into its own target with real coverage in alpha, and one premultiplied compose puts it back before the Done stage, so screenshots and the video recorder still record the finished frame. Always on for Vulkan; OpenGL draws its GUI onto the window as before
  • Refactor: VulkanDevice.cs split by subject (3456 lines into the device plus ten partials), the platform's duplicated state fields folded into the one stated-state record, the GPU tests no longer leak shader trees into /tmp
  • Per-pass GPU time from timestamp queries (OPTIMUM_VULKAN_PASS_TIMES=1, a stats.passes line per second) - no wait anywhere on the frame path. First table: at shadow quality 2 the shadow cascades are 60 % of GPU time; the far cascade is bound by draw count, not fill
  • Changing vsync on Vulkan no longer crashes: the OpenTK setter refuses a NoAPI window, so it routes to the swapchain's present mode
  • The temporal frame contract stays v1 for the native shaders (addendum A1: no member, resource, reset reason or adapter changed; both shader twins carry each clause), and a settings change re-links native programs without running the compiler, pinned by test

Left

  • Move the sharpen behind the final composition so bloom and god rays read the unsharpened scene
  • A per-block class flag for the AO (today it follows the shape's wind flag), and the resolve's clip softening fine per-texel texture
  • Refactor pass: consolidate the tests by subject, pacing measured against the OpenGL baseline, documentation
  • Shadow-caster split (ShadowCasterSplit, a second shadow program without the alpha test for the opaque groups): pixel-identical, 63 % faster on the near cascade, but 18 % slower on the far cascade at shadow quality 4 - net −1.5 FPS there, because that cascade is draw-count bound. Decide off-by-default or drop before merge
  • Verification on AMD and Intel hardware

Not in this PR, on the roadmap after it: shadow draw batching (the far cascade is 2,400 draws at a median of 200 triangles), camera-aware shadow-caster culling and occlusion culling, and a sweep of every platform member that depends on something the Vulkan window lacks (the vsync crash was one).

Conflict in patches/VintagestoryLib/Vintagestory.Client.NoObf/ClientPlatformWindows.cs.patch
resolved by porting branch 3's DisposeFrameBuffers HashSet dedupe into
build/VintagestoryLib (the source of truth, git-ignored) on top of the main
session's TAA shader-reload work, then re-running scripts/extract-patches.sh.
Injected static fields get no initializer (vanilla's static ctor runs), so the
per-renderer gate crashed the first entity frame with a NullReferenceException.
Verified in game on Vulkan and OpenGL; coverage test guards the lazy form.
…unwritten outputs, precise barrier accesses, validation that cannot be missed

- frame submit waits for the acquired swapchain image at all stages (first use is the present blit, a transfer)
- render-finished semaphore owned by the acquired image, not a rolling counter
- pipelines zero the colour write mask of every attachment the fragment shader never stores to (GL keeps contents, Vulkan wrote undefined values into the SSAO G-buffer under fullscreen passes)
- barrier access masks follow the layouts; read-only depth still makes its storeOp write available
- zero-instance draws are no-ops
- OPTIMUM_VULKAN_VALIDATION=1 logs to $TMP/optimum-vulkan-validation.log; OPTIMUM_VULKAN_VALIDATION_FEATURES=sync,best,gpu via VK_EXT_validation_features
- layer messages with braces no longer throw in the client logger

Verified: synchronization + best-practices validation in game shows no backend hazards; 322+8 GPU tests, Optimum.Tests green.
… pass culling, decals window)

ChunkRenderer.RenderLiquidMotion: useSSBOs, the matrix pop and the blend
restore all ran inside the try after the pool draws, so a throwing draw
left the renderer with SSBOs off, an unbalanced matrix stack and blending
off. The useSSBOs snapshot is now taken before the try and every restore
sits in the finally, guarded by a pushedMatrix flag.

ClientPlatformWindows.RenderOptimumSkyMotion: GlDisableCullFace() runs
before BeginMotionOnlyWrite(), so neither the early return nor the finally
undid it. Both paths call GlEnableCullFace() now.

SystemRenderDecals.OnRenderFrame3D: the GL setup, shader activation and
uniform setup ran between BeginMotionWrite() and the try; they are inside
the try now, so EndMotionWrite() always runs.

Verified: dotnet build VintageStory.slnx -c Release, scripts/extract-patches.sh
+ scripts/check-patches.sh (157 patches, 0 conflicts, 22 runtime donors),
dotnet test Optimum.Tests -c Release (923 passed) including three new
source-coverage assertions that the restores sit in the finally blocks.
Not verified in game (no launch in this worktree).
…all tests green

Adds the tooling P5 needs before the acceptance matrix can be run; no renderer
behaviour changes and nothing new runs on the GPU.

- scripts/dev/perf-capture.sh: launches through scripts/dev/run-client.sh with
  RENDERER=<backend>, optionally rewrites "Taa" in optimum.json, waits for the
  "[Client Chat] Welcome" line plus an 8 s warm-up, records a 30 s window, closes
  through scripts/dev/kill-client.sh and prints mean / 1% low / worst frame time
  plus a summary.csv. It re-reads the renderer out of the client log and refuses
  to report numbers on a silent OpenGL fallback (rule 1). It never pattern-kills.
- build/VintagestoryLib ClientMain: OptimumLogFrameTime writes one line per second
  ("[Optimum] fps window= frames= mean= min= max= p99=") when OPTIMUM_FPS_LOG names
  a file. Off otherwise - one null check per frame - so TAA off is unchanged.
  Backend-neutral, unlike OPTIMUM_VULKAN_STATS, which only exists on Vulkan.
  Members and the method are listed in Optimum.Patcher/Program.cs; the caller
  MainRenderLoop is an existing transplant target.
- scripts/dev/luma-diff.py: the still-frame luminance diff from the parity skill
  (centre 60% crop, --median over seven pairs), so the doc can name a real command.
- docs/taa-acceptance.md: the P5 matrix as a checklist - 17 visual rows plus
  byte-identical-off, performance and memory, each with scene, exact commands,
  pass criterion and the measurement to record; preconditions (creative, wind
  stilled, storms off, noon, clear sky); the P4 items carried into P5. docs/ is
  git-ignored, so .gitignore gains a docs/* + negation for this one deliverable.
- Optimum.Tests/taa-acceptance-harness-coverage-tests.cs: the doc names every
  matrix row TAA-PLAN.md lists, each row carries its four parts, the script drives
  the dev scripts and confirms the renderer, and the patcher ships the log members.

Verified: bash scripts/extract-patches.sh; bash scripts/check-patches.sh
(93 applied, 64 cecil, 0 pending/conflict; runtime patches 22 applied and exact
donors compiled); dotnet build VintageStory.slnx -c Release (0 errors);
dotnet test Optimum.Tests -c Release (929 passed, 34 skipped, 0 failed);
dotnet test Optimum.Render.Vulkan.Tests (330 passed, 0 failed).
NOT verified: the game was never launched (task prohibition), so the capture
script has not been run end to end against a live client.
Settings tab (GuiCompositeSettings): a TAA toggle plus sharpness and mip-bias
sliders, in the style of the existing Optimum rows. The toggle rebuilds the
frame buffers, reloads the shaders and requests a Toggle temporal reset (the
history targets and the motion attachment only exist while TAA is on, and a
history captured under the other configuration must never be reprojected);
both sliders apply live and carry hundredths because the element is an int.
The toggle guards on IsFeatureExplicitlyDisabled, like OptimumConfig.
EffectiveTaa, so a missing launcher scan cannot veto TAA. The three handlers
are listed in Optimum.Patcher/Program.cs; en.json carries the six strings; the
feature-flag build drops the row interval to 24px so 26 rows still fit the
fixed 740px main-menu dialog.

Scanner: the Taa verdict now covers taa-resolve, taa-debug, taa-sharpen, the
fsr pair the sharpen shares its maths with, any external file with the "taa-"
prefix and any file in the shaderincludes directory, not only vertexwarp.vsh.

Packaging: make deploy copies the whole sources/shaders directory (not *.fsh
plus *.vsh), creates the shaderincludes destination, and every packager plus
the deploy verifies that each source shader and include actually reached the
stage; scripts/package.ps1's staged-tree assertion names every TAA shader.

Verified: extract-patches + check-patches (93 applied, 64 cecil, 0 conflict;
22 runtime patches), dotnet build VintageStory.slnx -c Release, Optimum.Tests
970 (936 passed, 34 skipped, 0 failed), Optimum.Launcher.Tests 32 passed
(11 new scanner-behaviour cases), Optimum.Render.Vulkan.Tests 330 passed.
Not verified in game (launching is out of scope for this task).
…940 source + 333 GPU tests green

New taa-sharpen.vsh/.fsh (RCAS with a sharpness uniform; sharpness 0 is an
early-out bypass that returns the centre texel untouched, and the result keeps
its HDR range instead of clamping to 1). Registered like taa-resolve, optional
on compile failure.

RenderOptimumTaaSharpen runs right after RenderOptimumTaaResolve into a
dedicated RGBA16F target (index 21, allocated and released with the history
slots on both backends) and reassigns postSceneTexture, so bloom, god rays and
the Luma copy the final composition reads all see the sharpened image. It skips
itself when FSR 1's RCAS blit is going to run (render scale below 1): both the
pass and BlitPrimaryToDefault now ask the same OptimumFsrBlitActive(), so the
same pixels are never sharpened twice.

OptimumConfig.EffectiveTerrainLodBias adds TaaMipBias to the render-scale bias
while TAA is on; ChunkRenderer (atlas textures, both backends) and ShaderRegistry
(the chunkopaque/chunktopsoil sampler objects that override them) both read it.
A total of 0 - TAA off at native scale - still makes no parameter call at all.

Scanner vetoes Taa when a mod ships taa-sharpen, taa-resolve or taa-debug;
the latter two were missing from that list. Patcher entries for the new members.

Verified: extract-patches + check-patches (157 patches, 22 runtime donors),
Release build clean, dotnet test Optimum.Tests (940 passed) and
Optimum.Render.Vulkan.Tests (333 passed, validation layers on) - including a GPU
readback test that sharpness 0 is bit-for-bit identical to the input and that a
known edge gains local contrast monotonically with sharpness. Not verified in
game (no launch in this worktree).
…reports 43 runtime patches applied and exact donors compiled

The installed-launcher path decompiles the user's own VSEssentials.dll and
VSSurvivalMod.dll, applies patches/runtime/**, compiles that, and lets Cecil
transplant the bodies named in Optimum.Patcher/mod-patcher.cs. Every TAA P3/P4
mover had a mod-patcher Methods entry but no runtime donor, so installed players
would have got vanilla bodies: no motion vectors, camera-fallback ghosting, with
all fork tests green.

21 new donor patches, generated against a pristine .build/runtime-donors
decompile and matching the fork sources member for member:
- VSEssentials: EntityPlayerShapeRenderer (hand window), ModSystemFpHands
  (AnimationPrev UBO), ModSystemRenderFallingBlocksFast (per-entity previous
  matrix, one window around the loop).
- VSSurvivalMod: Quern, HelveHammer, Fruitpress, Resonator, Bloomery, Forge,
  Firepit, PotInFirepit (pot + lid), EchoChamber.
- Mechanics: MechBlockRenderer (NoteDevice, WriteInstance), MechNetworkRenderer
  (ApplyPassUniforms + window), AngledCageGear, AngledGears, GenericMechBlock,
  Transmission, Clutch, CreativeRotor, Pulverizer (OptimumInstanceMotion layout,
  instance counts).
EntityItemRenderer and EntityShapeRenderer donors regenerated to carry their
RenderItem/DoRender3DOpaque motion additions on top of what they already had.

Tests: new Optimum.Tests/taa-runtime-donor-coverage-tests.cs pins every fork
patch that adds a motion call to a runtime donor that adds the same calls, and
fails when a new instrumented fork patch has no donor mapping.
mod-patcher-manifest-consistency-tests.cs gains a Methods cross-check: every
transplanted method's declaring type must have a runtime patch or a sources/**
overlay, with the two FluffyClouds (Vulkan, not TAA) entries listed as known
gaps so a new one fails.

Verified: scripts/extract-patches.sh clean; scripts/check-patches.sh reports
"Runtime patches: 43 applied and exact donors compiled" (was 22); both donor
projects compile; dotnet build VintageStory.slnx -c Release; dotnet test
Optimum.Tests -c Release 951 passed; dotnet test Optimum.Render.Vulkan.Tests
330 passed. Donor coverage guards checked negatively by removing one patch.
No fork, lib, shader or manifest behaviour changed.
Merges the four P5 worktrees into feat/taa: sharpen pass + mip bias,
settings rows / scanner rules / shader packaging, runtime donors for the
P3/P4 mod movers, and the acceptance + performance harness.

Conflict resolution:
- Optimum.Launcher/ShaderCompatibilityScanner.cs: both branches extended the
  externalMotionShader veto. Kept the superset - taa-resolve, taa-debug,
  taa-skymotion and taa-sharpen named individually, plus the taa- prefix rule,
  the FSR pair the sharpen shares its lobe maths with, vertexwarp.vsh and the
  whole shaderinclude directory. The duplicate taa-resolve/taa-debug lines the
  two sides each added were folded into one.
- Optimum.Patcher/Program.cs merged both sides (sharpen members and program,
  the three settings callbacks, the fps-log fields and OptimumLogFrameTime).
- build/ and the API fork are per-worktree and git-ignored, so the winning
  sources were copied into the main tree before re-extracting: ChunkRenderer,
  ClientPlatformWindows, ShaderPrograms, ShaderRegistry and
  VintagestoryApi/Config/OptimumConfig.cs from the sharpen branch,
  GuiCompositeSettings from the settings branch, ClientMain from the harness
  branch. The mod-fork donor branch touches only patches/runtime/**, which
  extract does not regenerate.

Merge gap fixed: the Windows packager's explicit "what a release contains"
list was written before the sharpen shader existed, so taa-sharpen.vsh/.fsh
shipped only through the wildcard overlay and nothing asserted them.
Added to scripts/package.ps1 and to both lists in
taa-settings-coverage-tests.cs.

Verified: scripts/extract-patches.sh clean (157 patches, 0 stale);
scripts/check-patches.sh 0 pending / 0 conflict, runtime patches 43 applied
and exact donors compiled; dotnet build VintageStory.slnx -c Release 0 errors;
Optimum.Tests 987 passed / 34 skipped; Optimum.Render.Vulkan.Tests 333 passed;
Optimum.Launcher.Tests 32 passed. Game not launched.
…, NaN LOD-bias cache

Adversarial review of the P5 range (a04f409..f1a6300). Two defects found and fixed;
everything else in the range was checked and held (see below).

1. The TAA mip-bias row did not apply live to the passes it exists for.
   chunkopaque and chunktopsoil sample the block atlas through sampler OBJECTS,
   and a bound sampler object overrides the texture object's parameters on that
   unit - LOD bias included. The bias was written into those samplers only in
   ShaderRegistry.loadRegisteredShaderPrograms, i.e. once per shader load, while
   ChunkRenderer.ApplyOptimumTextureLodBias re-applied the atlas TexParameter
   every frame. So dragging the slider moved the mip selection of liquid,
   transparent and shadow terrain and left opaque terrain and topsoil on the bias
   they were compiled with - an inconsistent mip choice across passes of the same
   atlas, with the GUI tooltip and onOptimumTaaMipBiasChanged both promising
   "applies immediately".
   Fix: ShaderRegistry.ApplyOptimumTerrainSamplerLodBias(float) is now the single
   place those four samplers are written, called from the shader load as before
   and from ChunkRenderer.SetOptimumTextureLodBias, which already only fires when
   OptimumConfig.EffectiveTerrainLodBias actually moves. Both new members are in
   Optimum.Patcher/Program.cs.

2. ChunkRenderer.optimumTextureLodBias started at 0f, not NaN, so the first
   OnBeforeRenderOpaque of a TAA-off native-scale session read "0 wanted, cache
   not NaN" and wrote an explicit LOD bias of 0 over the driver default on every
   atlas - the one call the code comments and the coverage test say that
   configuration never makes. With fix 1 that would have reached every terrain
   sampler too. Initialised to float.NaN.

Regression tests:
- Optimum.Render.Vulkan.Tests/VulkanDeviceIntegrationTests
  ALodBiasWrittenToAnAlreadyBoundSamplerChangesTheMipTheGpuReads: a 4x4 mip chain
  with one flat colour per level, drawn at one texel per pixel so lambda is 0;
  SetSamplerParameter on the already-bound sampler moves the readback from level
  0 to level 1 and back. This is the backend half of the claim - the descriptor
  set has to key on the resolved VkSampler, not on the sampler id.
- Optimum.Tests/taa-sharpen-coverage-tests: ALiveMipBiasChangeReachesThe
  TerrainSamplerObjectsAsWell and TheCachedLodBiasStartsAtNanSoTaaOffTouchesNothing.
- fsr-pipeline-coverage-tests updated for the extracted sampler helper.

Checked and NOT changed: TAA-off byte-identity (the FXAA/Luma else branch is
vanilla; index 21 and the sharpen pass are both gated); the no-double-sharpening
rule (one shared OptimumFsrBlitActive(), term-for-term the old inline condition);
sharpness 0 (early-out before the first ring tap, GPU-proven bit-identical);
scanner verdicts (taa- prefix plus the whole shaderincludes directory, external
sources only); packaging (all five packagers, the Makefile wildcard-plus-verify,
package.ps1's required-file list); runtime donors (43 applied, exact donors
compiled, marker parity per fork patch); patcher entries; and the translation gate
- taa-sharpen is a program pair in sources/shaders, so ShaderCorpus.ProgramNames
picks it up and EveryVanillaProgramTranslatesToSpirv covers every variant row.

Verified: extract-patches + check-patches (93 applied, 64 cecil, 0 pending; 43
runtime patches, exact donors compiled), Release build, 989 source tests, 334 GPU
tests, 32 launcher tests. Not verified in game - no deploy, no client run.
What P5 landed (sharpen pass, no-double-sharpening rule, mip bias, settings rows,
scanner rules, packaging verification, the installed-runtime donors that close P3
finding (f) / P4 finding (t), and the acceptance + performance harness), the exact
vs fallback table updated for it, findings (ab)-(af) from the adversarial review,
and the full "still owed in the game" list.

The acceptance matrix has not been run - no phase of P5 deployed or launched the
client - so P5 is not done by rule 3 and the default-on decision is left to the
user with docs/taa-acceptance.md as the gate. TAA stays opt-in.
perf-capture and rule 1 both key on the '[Optimum] <backend> renderer' line; an explicit OpenGL choice, and a Vulkan start without an advisory, printed nothing.
docs/temporal-frame-contract.md is now the frozen, versioned (v1) specification
every temporal consumer is written against: the in-house TAA resolve today,
FSR 3.1 / XeSS 2 / DLSS next, frame generation and ray reconstruction after that.

It specifies the per-frame input record member by member (type, units, coordinate
convention, the point in the frame after which each value is this frame's), every
resource with format, resolution, sampler state and channel semantics - the motion
attachment's rg/b/a including the writer-depth validity tolerance and its
half-float rationale, the history colour/glow/linear-depth slots and why their
filters differ, the sharpen target - the jitter definition and Halton sequence,
the reset reasons with their triggers, the per-class exact/fallback/reactive
status consolidated from P3-P5, and the adapter formulas for the three vendor
upscalers (motion vector scale and sign, jitter sign, depth convention,
reactive/transparency mask mapping, exposure, camera constants). Native handles,
extension negotiation, presentation lifetime and ray-reconstruction guides are
explicitly reserved for the vendor plan.

Optimum.Tests/temporal-contract-tests.cs is the tripwire: it pins the public
surface of IOptimumTemporalContext and OptimumTemporalFrame by reflection against
a checked-in list, and pins the conventions the document states as fact - the
shear formula in both implementations and numerically, the motion-vector scale
and sign per adapter, the writer-depth tolerance expression in taa-resolve.fsh,
the resolve's full input set, the history/sharpen slot indices and parity rule,
and the attachment formats and sampler state on both backends. Every failure
message names the document, and the surface assertion prints the actual list so
the new one is the failure message.

Verified: extract-patches + check-patches (93 applied, 64 cecil, 0 pending; 43
runtime donors exact), dotnet build VintageStory.slnx -c Release, dotnet test
Optimum.Tests -c Release (1007 passed, 34 skipped). Negative control run: removing
one member from the checked-in surface list fails TheTemporalContextSurfaceIsFrozen
with the document named. No game run - this phase changes no rendering code.
…itch reset, ChunkRenderer motion windows exception-safe

- ClientPlatformWindows: both frame-buffer setup paths (device and GL) now read
  `!optimumTaaDisabled && OptimumConfig.EffectiveTaa`, so a platform that already
  failed the TAA allocation never retries it on a later rebuild. EffectiveTaa
  alone already goes false (DisableOptimumTaa always calls DisableTaaAtRuntime),
  so this is a belt-and-braces guard on the per-instance flag; the stale doc
  comment on DisableOptimumTaa that claimed EffectiveTaa stays true was corrected.
- GuiCompositeSettings.onOptimumTaaChanged: the explicit-disable bail-out now
  resets the already-flipped switch to EffectiveTaa before returning, so the UI
  cannot show TAA on while it is off for the session.
- ChunkRenderer.RenderOpaque and RenderAfterOIT: the motion-vector windows are
  wrapped in try/finally (matching RenderLiquidMotion), so a throwing shader
  setup or pool draw can no longer leak the expanded draw-buffer mask into every
  later draw and block every later window. Draw order and state calls unchanged;
  GlPopMatrix left where it was.

Verified: dotnet build VintageStory.slnx -c Release (0 warnings, 0 errors);
scripts/extract-patches.sh + scripts/check-patches.sh (0 pending, 0 conflict,
157 patches, 43 runtime donors exact); dotnet test Optimum.Tests -c Release
(1010 passed, 0 failed, 34 skipped), including the three new coverage tests.
All changed methods were already listed in Optimum.Patcher/Program.cs.
…f pairing, --vsync validation, doc corrections

Applies the review findings scoped to the launcher scanner, the Makefile deploy,
the dev scripts and the TAA docs.

- ShaderCompatibilityScanner.NormalizeShaderPath: the stage-extension filter
  (.fsh/.vsh/.gsh) now applies to shaders/ only. ShaderRegistry loads every
  shaderinclude regardless of extension and vanilla ships five .ash includes, so
  an external .ash override was invisible and TAA stayed on with a replaced
  helper. Directory entries (no extension) are still rejected.
- Makefile deploy: shader and shaderinclude copies are per-file with
  `cp -f ... || exit 1` instead of `find -exec ... \;` / a bare wildcard cp
  (find returns 0 when an individual cp fails), and the completeness check
  compares CONTENT with `cmp -s` instead of `[ -f ]`, which passed against an
  unchanged vanilla file of the same name. Both the VANILLA_DIR and the
  INSTALL_DIR block.
- scripts/dev/luma-diff.py: --median now pairs non-overlapping files
  (files[::2] with files[1::2]) instead of every adjacent pair, errors on an odd
  count, and the docstring plus the docs/taa-acceptance.md invocation say to pass
  files in pair order (a flat `shots/*.png` glob sorted a1..a7 before b1..b7).
- scripts/dev/perf-capture.sh: --vsync is validated like --taa ("", on, off),
  exit 2 to stderr; "--vsync 0" or a typo silently meant vsync ON.
- docs/taa-acceptance.md P2: the slot-21 sharpen target (OptimumTaaSharpenIndex
  = 21, RGBA8 the size of Primary, ~15.8 MiB at 1080p) added to the memory
  budget; total ~86.9 MiB.
- docs/temporal-frame-contract.md: escaped |CameraPosDelta| in the Teleport row
  (it read as extra table cells); new section 6.1 declaring cloud pixels
  UNSUPPORTED for external motion consumers (mv is camera-rotation-only, reject
  via the reactive mask; a real vector needs prev cloudOffset + the ray-marched
  hit from cloudvolumetric.fsh, future work). No shader changed.
- TAA-PLAN.md: finding (n) carries the same contract term; finding (t) rewritten
  - the eight mover donors landed in P5 (c897e23, merged 0120422) under
  patches/runtime/VSSurvivalMod/Vintagestory/GameContent/ (MechNetworkRenderer
  under .../Mechanics/), guarded by Optimum.Tests/taa-runtime-donor-coverage-tests.cs
  and mod-patcher-manifest-consistency-tests.cs; the two P4 status paragraphs no
  longer claim "not verified in game on either backend" - the entity
  motion-writer gate was verified on Vulkan and OpenGL (2830577) and P5's
  in-game run (7b0168d, deployed c9758ce+5b952da) drove both backends, while the
  per-class mover behaviour and the 18-row acceptance matrix stay unverified.

Verified: dotnet test Optimum.Launcher.Tests -c Release -> 34 passed, 1 failed
(SolutionIntegrityTests.EverySolutionProjectPathExists, pre-existing and only
because the git-ignored build/ tree is absent in this worktree); the scanner
filter alone is 18/18 green. `make -n deploy` exits 0 and the rewritten copy and
cmp snippets were run against a temp directory: a clean copy passes, a tampered
destination and a deleted destination both fail with the new message.
`bash -n scripts/dev/perf-capture.sh` clean and
`perf-capture.sh --renderer vulkan --vsync bogus` exits 2 before any config
write. luma-diff.py run on generated 64x64 PNGs: 2 pairs -> median 5.000, odd
count -> SystemExit. Not run here: extract-patches/check-patches and the game
(no ignored trees in this worktree).
…pply inside the window; runtime donors mirrored

Verified: dotnet build VintageStory.slnx -c Release (0 errors);
extract-patches + check-patches (93 applied, 0 pending, 0 conflict;
43 runtime patches applied and exact donors compiled);
dotnet test Optimum.Tests -c Release (1011 passed, 0 failed, 34 skipped).
…t masking, dead UBO buffer

- CreateInstance: enable VK_EXT_validation_features whenever the
  ValidationFeaturesEXT struct is chained into pNext; the feature list is now
  parsed (ParseValidationFeatures) before the extension array is marshalled so
  both decisions use the same condition. A chained struct with the extension off
  was silently ignored.
- ProgramInterfaceLayout: fragment output arrays with only a constant element
  stored to now mark just that element's location written
  (TryGetWrittenFragmentOutputElements); dynamic indices, whole-array and
  swizzled stores keep the full span, and an index on a non-array output still
  means the whole variable. PipelineCache no longer leaves colour writes on for
  an attachment the shader never writes (undefined data on Vulkan).
- VulkanDevice: ResolveValidationLogPath accepts a Windows path too, and the
  default-path field now precedes the field that reads it (static initialiser
  order made the bare OPTIMUM_VULKAN_VALIDATION=1 log path null).
- MirrorValidationMessage also swallows UnauthorizedAccessException,
  NotSupportedException and ArgumentException; a diagnostic write must not abort
  Initialize.
- ClientUniformBuffer: removed the per-UBO VulkanBuffer, SyncBuffer, the
  descriptor Release and the deferred disposal. Verified dead: no path binds
  ubo.Buffer - the ring-exhausted path allocates its own transient copy - and
  SyncBuffer had no caller anywhere in the tree.
- TaaResolveTests: all eight LoadProgram sites are 'using' now, so the programs
  are destroyed before the context.

Verified: dotnet build Optimum.Render.Vulkan.Tests -c Release (0 warnings,
0 errors) and dotnet test Optimum.Render.Vulkan.Tests -c Release on the GPU with
validation layers on: 349 passed, 0 failed, 0 skipped (ShaderTranslationTests
and the pipeline/attachment tests included). Not run here: the game, make deploy,
extract/check-patches (ignored trees absent in this worktree).
Test-only changes, no production code touched.

- fsr-pipeline / taa-sharpen: "if (textureLodBias == 0f)" now has to be the
  restore path it actually is - the float.NaN cache, the !IsNaN guard,
  SetOptimumTextureLodBias(0f) and the cache reset, all asserted inside that
  branch (brace-matched, so the nonzero path below cannot satisfy them). The
  sampler entry point is asserted to apply the raw value: no "!= 0f" inside
  ApplyOptimumTerrainSamplerLodBias, all four samplers written with `bias`.
  Read ShaderRegistry.cs:315-343 first: the sampler path does NOT skip zero, so
  there is no production gap here.
- New PatchMethodScopes: locates a patch's added lines inside the tree it was
  applied to and attributes them to the enclosing method (brace scanner that
  skips comments and string literals). Covered by patch-method-scopes-tests.cs.
- mod-patcher-manifest-consistency: EveryTransplantedMethodHasARuntimeDonor is
  per METHOD now, not per declaring type. A type whose donor patch touches only
  some other method is reported separately (KnownUnchangedTransplants, seeded
  with ChunkMapLayer::loadFromChunkPixels, whose body really is unchanged in
  both trees). KnownDonorGaps keeps its meaning. Degrades to the old per-type
  check when neither .build/runtime-donors nor the fork is on disk.
- taa-runtime-donor-coverage: marker sets are compared per method as well as
  file-wide, so a donor that writes the same markers into a different body no
  longer passes.
- taa-terrain-motion: the prepass test reads through ReadPatchedOrSource like
  the rest of the file (it would have thrown in a clean clone); the vanilla
  projection line, which sits outside every hunk, is asserted against the
  decompiled tree only when that is checked out.

Verified: dotnet test Optimum.Tests -c Release - 1015 passed, 0 failed, 34
skipped. Negative check on real data: moving the Optimum block of
QuernTopRenderer out of OnRenderFrame in .build/runtime-donors made both
strengthened tests fail with the right messages (donor coverage listed the
three misplaced markers; the manifest test named
QuernTopRenderer::OnRenderFrame); donor restored afterwards.
…ged Makefile

The scripts/docs stage replaced the deploy shader copy (find -exec cp, then a
bare [ -f "$d" ] completeness check) with a per-file cp loop that aborts on
failure and a cmp -s content comparison. MakeDeployCopiesEveryShaderAndFails-
WhenOneDoesNotArrive still pinned the old literal, so the merge of the two
branches broke it - a semantic conflict, no functional regression.

The assertions now pin the stronger contract: both deploy paths loop over the
whole sources/shaders and sources/shaderincludes directories (not *.fsh plus
*.vsh), each cp is '|| exit 1', and both completeness checks compare content
with cmp -s rather than existence, with a DoesNotContain on the old [ -f ] form.

Verified: dotnet build VintageStory.slnx -c Release 0 errors; extract-patches
wrote 157 patches with no tree change; check-patches 0 pending / 0 conflict,
43 runtime donors exact; Optimum.Tests 1015 passed 0 failed 34 skipped;
Optimum.Launcher.Tests 35 passed 0 failed; Optimum.Render.Vulkan.Tests 349
passed 0 failed on a real GPU with validation layers on.
Resolved: patcher listings, platform seam manifest, coverage tests and the design doc
keep every stage's entries (the GUI section is renumbered 3d). ClientPlatformAbstract's
generated patch is regenerated from build/ at the end of the integration.
…d build/ tree

The five stage branches each inserted their seams at the same point in
ClientPlatformAbstract, so the generated patch conflicted on every merge. Resolved by
rebuilding build/ from patches, adding the entity, sky/particle/decal and GUI seams to
the source, and re-extracting, so the hunk offsets are the extractor's own.

Verified on the merged state: build Release 0 errors; extract-patches 159 patches /
202 valid; check-patches 93 applied, 66 cecil, 0 pending, 0 conflict, 43 runtime patches
exact; Optimum.Tests 1271 passed / 0 failed / 34 skipped; Optimum.Render.Vulkan.Tests
1107 passed / 0 failed with the implicit-layer disable set (only VK_LAYER_MESA_device_select
inserted).
Owner, 2026-09-16: the workflow approach does not work for them - they cannot
see what is happening. Rule 8 now says the session does every step itself, in
order, visibly; subagents only for read-only search. Rules 14-16 rewritten
accordingly. Rule 19 added: build and tests compile against the fork and miss
a wrong transplant parameter count and a lib call to a fork-only API member,
both of which shipped today as crashes; make patch-il and the fork diff catch
them and run after every lib or fork change.
…member

The shipped client threw MissingMethodException
'MeshRef MeshDataPool.get_ModelRef()' at SystemRenderDecals.OnRenderFrame3D on
both backends: the particles-sky stage added a public ModelRef to the API fork
and made the lib call game.Platform.RenderDecalPool(decalPool.ModelRef, ...).
A new public member on a vanilla API type never ships - the shipped
VintagestoryAPI-patched.dll is vanilla plus the hooks in api-patcher.cs - and
modelRef is internal in vanilla, so the lib could not read it either.

The decals now use the scope shape the chunk pools already use:
BeginDecalPass(decalTextureId, blockTextureId) / EndDecalPass() on
ClientPlatformAbstract with empty neutral bodies, the VANILLA
decalPool.Draw(game.api, game.frustumCuller, CullInstant) between them, and the
Vulkan RenderMesh(MeshRef, int[], int[], int, bool) override routing the pool's
multi-draw to TryDrawDecalPoolNative while the scope is open. OpenGL is vanilla
again. The scope closes in the same finally as the motion window, ahead of it.

Also fixes a transplant tuple in the same merge that make patch-il caught:
ClientMain::RenderTextureIntoFrameBuffer is 10 parameters, not 9, and the
patcher aborted on it before reaching the end.

Verified: dotnet build VintageStory.slnx -c Release clean; extract-patches +
check-patches 159 patches, 0 pending, 0 conflict; Optimum.Tests 1271 passed, 0
failed, 34 skipped; Optimum.Render.Vulkan.Tests 1107 passed, 0 failed with the
implicit layers disabled; make patch-il "207/207 required methods patched".
Not verified: the game was not launched (agents do not).
… opt-in; draw-state trace

The stage-2 merge shipped with the first-person hand drawn wrong on Vulkan
with TAA on: the native entity route shows the joints the OpenGL body hides.
Bisected in the real client with new per-route switches
(OPTIMUM_VK_NATIVE_{CHUNKS,ENTITIES,WORLD,SKY,GUI}): only the entity route,
only with TAA on; TAA off matches OpenGL. A new render-trace line in
BindStorageSet prints, per draw, the record offset, every named block's
buffer and offset and the projection/view/model matrices by name - the two
routes bind byte-identical state for that draw, so the defect is in the
TAA-on path (motion window, TAAMOTION outputs, or the motion-writer hooks the
neutral RenderMesh body ran and the native route bypasses). Entities are
opt-in (OPTIMUM_VK_NATIVE_ENTITIES=1) until that is fixed; the finding and
the repro are in the handoff.

Also: the stale test assertion for the corrected transplant tuple.

Verified on the deployed build, both backends headless, TAA on: 0 client
errors, validation 0 errors and 0 SYNC-, entity GPU tests 24 passed, scene
SSIM 0.9838/0.9736/0.9820 against a same-session GL-vs-GL floor of
0.9961/0.9853/0.9932.
…only; on by default again

VSEssentials registers its own entityanimated for the first-person hands, with
its own Animation block and, under TAA, its own AnimationPrev block. Through
the native route that program drew the arm several times too large with TAA
on. Bisected in the real client: vanilla entity programs native with the hand
on the neutral body matches OpenGL (0.9825/0.9835/0.9810). The render trace
shows every bound input of that draw equal on both routes, so the cause is
still open; it is recorded in the handoff with a repro.

Phase 3b decision 1 already puts mod renderers on the adapter, so the route now
takes only ShaderPrograms.Entityanimated and Shadowmapentityanimated and is on
by default again. OPTIMUM_VK_NATIVE_ENTITIES=0 turns it off,
OPTIMUM_VK_NATIVE_ENTITIES=all admits mod programs for the investigation.

The trace line in BindStorageSet now also prints block content hashes, the
record and push hashes and the frame block version and hash.

Tests: the entity fixture builds the vanilla program type and registers it
where the client does; new AModRegisteredEntityProgramStaysOnTheNeutralBody.
Coverage pins the per-route environment switches and the vanilla-only rule.

Verified: entity GPU tests 25 passed, coverage 28 passed, make deploy 207/207,
headless Vulkan with TAA on 0 client errors, validation 0 errors and 0 SYNC-;
Vulkan vs OpenGL 0.9631/0.9617/0.9658 against a same-session OpenGL floor of
0.9853/0.9750/0.9785, frames equal on inspection, residual is run timing.
…o longer vacuous

New seam ClientPlatformAbstract.RenderSunQuad for the sun disc in
SystemRenderSunMoon.OnRenderFrame3D, neutral body the RenderMesh it replaced.
The Vulkan override takes only the registered vanilla standard program and
resolves every sampler the pipeline declares (standard reads frame textures
too). The occlusion-query probe keeps RenderMesh: a Vulkan occlusion query has
to begin and end inside one render pass, and a native draw opens its own.

Every pixel comparison in NativeWorldSystemsTests was between two untouched
attachments: RunFrame bound programs directly, so the frame block stayed zero
(standard.vsh divides by viewDistance and discards everything) and the model
and view matrices stayed zero (every vertex collapses). The fixture now seeds
both, and the comparison helper fails if the scene slot is still the clear.
That exposed a fixture asymmetry - the decal tests never bound the atlases to
units as ShaderProgramDecals' setters do - fixed; decals match with real
pixels. The particle tests remain vacuous (no per-instance data in the quad)
and say so.

Verified: world/sky/entity GPU tests 38 passed with real pixels (sun
122,118,18; decals 15,31,19), Optimum.Tests 1271 passed, patches 159 with 0
conflicts, make patch-il 207/207, headless Vulkan 33 native sun passes, 0
client errors both backends, validation 0 errors and 0 SYNC-. The residual
Vulkan-vs-OpenGL SSIM is the same with every stage-2 route switched off
(0.981/0.971/0.964), so stage 2 did not introduce it.
The quad pool goes through the native route under the Transparent target's
blend contract the chunk route already records (weighted accumulation,
revealage, glow), with depth test on and depth writes off - the state
LoadFrameBuffer(Transparent) sets. Vanilla ShaderPrograms.Particlesquad only;
falls back while the contract is unrecorded or the target is not Transparent.
particleTex resolves from the program's declaration; particlesquad.fsh never
samples it.

Checked: build 0 errors, coverage 29 passed, deploy 207/207; headless Vulkan
with precipitation forced on: 129 native OIT particle passes, 0 emulated quad
particle draws, 0 client errors, validation 0 errors and 0 SYNC-, snowfall
renders.
…T passes dropped the accumulation slots

GUI quads. New seam ClientPlatformAbstract.RenderGuiQuad for every
Render2DTexture overload (four transplanted ClientMain methods, the two
seven-parameter overloads told apart by parameter types). The native route
takes the vanilla gui program and draws under the state the client last stated
through the platform's own virtuals - blend on and mode, depth test, depth
mask, depth function, scissor - recorded in VulkanClientPlatform.State.cs,
never read off the tracker. Native passes gained an optional scissor.
Headless: 992 native GUI quad passes per run, HUD renders; 360 gui draws
remain emulated (the other GUI systems).

OIT fix. BeginOitAccumulation selects six draw buffers and keeps the colour
accumulation on slots 3-5, which the Transparent FrameBufferRef does not list.
The native chunk OIT groups and the quad particles derived their slots from
that texture count (0x7), so every native OIT draw darkened revealage and added
no colour - black snowflakes were how it showed. The slot set is now recorded
where the client selects it (0x7 in ApplyTransparentPassBlendState, 0x3F in
BeginOitAccumulation) and used by both routes. Snow renders white again;
chunk-oit-liquid, chunk-oit-transparent and the particle passes declare 0x3F.

Checked: build 0 errors, Optimum.Tests 1274 passed, GUI/world/chunk GPU tests
26 passed, patches 159 with 0 conflicts, make patch-il 211/211, headless
Vulkan 0 client errors and validation 0 errors / 0 SYNC-.
VulkanClientPlatform.RenderMesh takes any draw under ShaderPrograms.Gui
natively - block highlights, the wireframe, gear and progress overlays, mods
drawing with the gui shader through IRenderAPI.RenderMesh - under the state the
client stated through the platform's virtuals, now including cull mode and line
width, with the program's declared textures. It honours NativeGuiEnabled, so
the seams' neutral bodies and the old-route switch still reach the emulated
draw.

Checked: build 0 errors, GUI GPU tests 7 passed, coverage 32 passed, deploy;
headless Vulkan: 992 native GUI quads and 8367 other native gui draws per run,
0 emulated gui draws, 0 client errors, validation 0 errors / 0 SYNC-, HUD
renders.
… native

VulkanClientPlatform.RenderMesh takes any draw under ShaderPrograms.Standard
on a world target natively - held and dropped items, block entity models -
under the state the client stated (blend, depth test and mask, depth function,
cull) with GlToggleBlend's exceptions applied per attachment: SSAO G-buffer
slots and the open motion attachment replace, the Transparent target keeps its
recorded OIT contract. Samplers resolve from the program's declared textures.
Never inside an occlusion query (a Vulkan query must begin and end inside one
render pass - the sun probe keeps the emulated draw) and never on the default
framebuffer (GUI item icons stay emulated for now).

Checked: build 0 errors, coverage 33 passed, world/GUI GPU tests passed, deploy;
headless Vulkan: 360 native standard draws, 0 emulated, 0 client errors,
validation 0 errors / 0 SYNC-, frame free of artefacts against the old route.
RenderMeshInstanced under the vanilla particlesquad2d program becomes an
instanced native pass on the bound target (the default framebuffer in the
menu) with the state the client stated. DrawNativeGuiMesh gains an instance
count.

Checked: build 0 errors, deploy; headless Vulkan: 1766 native 2D particle
passes, 0 emulated, validation 0 errors / 0 SYNC-, no client errors; menu
screenshot on Vulkan renders the background and particles.
… the sun probe included

- the colour mask the client stated (GlColorMask) is recorded and applied per
  slot on the native standard route;
- draws into the default framebuffer (GUI item icons) get their own native pass
  (TryRenderStandardMeshToDefault) with the stated state and scissor;
- the sun's occlusion probe no longer falls back: a native pass opens its scope
  through the target manager, whose scope hooks suspend and resume the open query.
- coverage assertion for the 2D particle route updated to the instanced draw.

Checked: build 0 errors, coverage 33 passed, GPU world/GUI/query tests 21
passed, deploy; headless Vulkan: remaining emulated draws are guigear,
cloudmap, cloudvolumetric, the first-person hand and the loading screen only;
validation 0 errors / 0 SYNC-. The spawn view in the save currently renders
white on both routes (all native routes off gives the same frame), so no
visual comparison of the world was possible in that scene.
RenderMesh under the platform's hardcoded ShaderProgramMinimalGui (no pass
name; MainMenuRenderAPI.Render2DTexture before the shader registry is up)
goes through DrawNativeGuiMesh with one sampler and the stated state. The
drawer compares a null pass name as "".

Checked: build 0 errors, coverage 34 passed, GPU GUI tests 7 passed, deploy;
headless Vulkan: 10 MinimalGui trace lines, emulated draws left are guigear,
cloudmap, cloudvolumetric and the first-person hand only; validation 0 errors
/ 0 SYNC-. Not checked visually: the quads show for a few startup frames.
- guigear (HudHotbar's temporal stability gear, a plain RenderMesh of the GUI
  quad) takes the single-sampler GUI mesh route with MinimalGui;
- a native draw that samples a colour attachment of its own target takes the
  pooled ReadSelf copy the emulated route takes (SnapshotColorAttachment)
  instead of being refused, and the bindless resolve samples the copy. This is
  the atlas composition: BlendedTextureManager and RenderTextureIntoFrameBuffer
  copy one atlas region into another of the same atlas. A non-colour
  attachment of the own target is still refused.

Checked: build 0 errors; GPU: new self-blit differential test (left half
copied into the right half, native equals emulated, 2 native draws, 0
emulation calls, validation clean), 298 native/feedback GPU tests passed;
coverage 34 passed; deploy; headless Vulkan: emulated draws left are
cloudmap, cloudvolumetric and the first-person hand only; validation 0 errors
/ 0 SYNC-; gear crop matches the old route's look.
- VulkanForkGraphics records the depth test, blend and raw framebuffer binds it
  forwards, on the platform, so a native route runs with the fork's state;
- cloudmap draws into the fork's own framebuffer (formats from the attachments
  its SetDrawBuffers enabled), blending and depth off;
- cloudvolumetric draws onto the Transparent target under the OIT contract,
  samples Primary's depth as the bound depth (no depth writes, as GL with the
  depth test off) and resolves liquidDepth to the LiquidDepth target's depth:
  resolved to 0 it replaced the frame's liquid depth with a placeholder, which
  drew no clouds and an orange halo around the sun;
- OPTIMUM_VK_NATIVE_CLOUDS=0 keeps both on the emulated route;
- NativeWorldPrepare takes samplesBoundDepth.

Checked: build 0 errors, coverage 35 passed, GPU world tests 13 passed,
deploy; headless Vulkan, cumulus at noon, AO debug view off for the run:
native clouds against OpenGL SSIM 0.969 (cloud route off: 0.879), cloud
patch and sun match OpenGL by eye; validation 0 errors / 0 SYNC-; only the
first-person hand is still emulated.
…aw is native

The entity route admits the program the shader registry holds under the
entity pass names - VSEssentials' first-person hand program replaces the
registry entry for entityanimated. The registry is only asked about a program
it registered (PassId set), since its type initializer rewrites
ShaderPrograms.*; the cloud route uses the same check.

The 2026-09-16 fault (the arm drawn several times too large with TAA on) no
longer reproduces with its repro (OPTIMUM_VK_NATIVE_ENTITIES=all,
OPTIMUM_VK_NATIVE_SHADERS=force, TAA on): parity dump of the hand region at
frame 300 against the neutral body - depth identical, motion within 0.0023,
G-buffer within 0.0039. The cause was never named; recorded in the progress
doc.

Checked: build 0 errors, coverage 35 passed, GPU entity/world/GUI tests 27
passed, deploy; headless Vulkan with default route settings and native
shaders forced: 0 emulated draws, 58 native Opaque entity draws, validation
0 errors / 0 SYNC-, no exceptions.
…CAS_DENOISE)

The GTAO term's TAA-converged residual on flat faces (0.24/255 per-pixel
temporal std, three times the no-AO baseline of 0.08) was multiplied 2.7x by
the sharpen at strength 1.0 and drawn as grain: an RCAS port without AMD's
noise limiter, which FSR 3 ships enabled. The lobe is now scaled by
1 - 0.5 * (the centre's deviation from the mean of its four neighbours over the
ring's range): a lone deviation gets half the lobe, an edge keeps 7/8.

Eliminated on the way, with numbers in the tests and the branch doc: the AO's
noise is Monte-Carlo (medium raw 10 % -> denoised 2.7 % per frame on flat
faces; ultra 4x less), the reconstruction, normals, depth mips and row order
match the G-buffer, and the resolve keeps 15 % of one frame's noise (new
TaaResolveTests.PerFrameNoiseOnAStaticSurfaceAveragesOut, which also records
the variance clip's ~3 % softening of a +-5 % per-texel texture).

Checked: headless Vulkan, flat-face grain with GTAO now equals the AO-off image
(spatial HF 2.05 vs 2.04, was 2.82), flicker 0.64 -> 0.44; GPU tests for the
sharpen (new lone-pixel test: 0.6 on 0.3 sharpens to 0.72, not 0.9; the edge
test still holds), the resolve and the post chain pass; sharpen coverage 20
passed; the owner judged it in game ("worlds better").
…blend-no-cull pool

The chunk shader forced gNormal.w = 1 (the 0.05-block occluder class) for every
draw with haxyFade set, meaning the entire blend-no-cull pool. That pool holds
solid blocks too - snow layers - so in a snow-covered world 59 % of the visible
pixels, 158 of 168 flat faces, were thin occluders. A sentinel written from a
per-pool uniform proved the pool (not RenderAfterOIT, which is the water-plant
pass). The override is removed on both shader routes; the class is what vanilla's
vertex stage already writes from each shape face's WindMode.

Not the grain (turning the class off made the AO darker and noisier), and in this
world it changes little: the snow-covered soil tops carry a WindMode in their
shapes, so vanilla's flag still marks most of them. A per-block class needs a
tesselator flag; open.

Checked: build 0 errors, 49 programs / 142 variants compiled, patches 0
conflicts, 211/211 methods patched, AO coverage 23 passed, chunk/AO GPU tests 22
passed, deploy; headless Vulkan: flat-face temporal std 0.45 / spatial HF 2.02
(before 0.44 / 2.05), validation 0 errors / 0 SYNC-.
…tory

AGENTS.md, CLAUDE.md, .claude/, the Vulkan-native plan, the branch status
notes and scripts/dev/harvest-maps.py are ignored and untracked; they stay
beside the checkout. Code, tests, shaders, scripts and the tracked docs no
longer point at them or at review sessions: each reference now states the rule
it meant (a launch is not a verification; a clear on a masked attachment is a
no-op on every path; the resolve's temporal invariants; the still-frame
luminance diff). TAA-PLAN.md keeps its content and drops the review-session
mentions.
The parity capture's reworded comment must not spell the process-kill command
the test forbids, and the seam-deletion test names the fork bridge's current
two-argument constructor (platform, device) from the cloud work.

Checked: Optimum.Tests 1278 passed, 34 skipped.
…, mesh part pool, installer

51 upstream commits. The merge was done on the sources, not the generated
files: every file upstream touched was three-way merged (ours, merge-base,
upstream) on one common baseline and the patches regenerated from the result,
because regenerating from a tree that had only some of upstream's changes
silently dropped the rest (caught by upstream's own patch-reading tests).

Baseline: upstream decompiles with ilspycmd 11.0.0.9375; 84 lib files
differ from the 10.1 decompile, 18 of them carrying our patches. build/ was
rebased onto the 11 snapshot (two files needed a hand merge, ShaderRegistry
and EventManager: our edits against the decompiler's own reformulation of the
same lines) and every patch is regenerated against it, so the maintainer's
bootstrap applies them.

Resolutions: the frame end keeps EndFrame() (the platform seam) followed by
upstream's RecordChunkRenderFrame(); OptimumConfig takes both sides' new
members; the patcher lists both sides' targets (224 methods now); the
overlay packager ships the shader includes with the completeness check the
other packagers have; the vanilla-regions test lists upstream's indirect-draw
members.

Checked: build 0 errors, patches 0 conflicts, 224/224 methods patched,
Optimum.Tests 1343 passed (34 skipped), GPU suite 1116 passed, deploy;
headless on both backends in the real save: no exceptions, Vulkan validation
0 errors / 0 SYNC-, frames render.
…on removal, step 1)

Every draw the dedicated native routes do not take - mod renderers with their
own programs, the vanilla programs without a dedicated route, the seams'
neutral bodies behind the route switches - is now recorded natively before
the emulated draw is reached: RenderMesh, RenderMeshInstanced, the pool
multi-draw and the fullscreen triangle (VulkanClientPlatform.NativeStated.cs).

It builds the pipeline, the pass and the textures from StatedRenderState, the
fixed-function state the client stated through the platform's virtuals, with
OpenGL's semantics: glBlendFunc sets every draw buffer, glBlendFunci one,
glDisable(GL_BLEND) keeps the functions; the colour mask is global; draw
buffers are per framebuffer (attachment 0 by default) and become write masks;
one texture per unit; a framebuffer bind does not move the viewport. Every
recording site forwards to the device as before (StateDrawBuffers,
StateSlotBlend, StateBlend, the fork bridge's notes), so the emulated route is
unchanged until step 2. Textures resolve per sampler through the program's
unit mapping, as the emulated draw does; the pass declares every colour slot
attached on the device.

Found on the way: the OIT accumulation targets are attached to Transparent
without being in its ColorTextureIds (water drew black until the slots came
from the attachments); GL's thick-line probe leaves the line width at 1.5;
GlToggleBlend(false) keeps the functions on GL while the device reset them.

OPTIMUM_VK_NATIVE_STATED=0 keeps the emulated route; OPTIMUM_VK_STATED_CHECK=1
compares the record against the device's tracked state at every draw.

Checked: build 0 errors; new NativeStatedTests (blend off, three modes, colour
mask, scissor, an unselected draw buffer: pixels equal to the emulated route,
one native draw, no emulated call inside the pass); the dedicated-route
differential fixtures keep the emulated neutral body; GPU suite 1123 passed,
Optimum.Tests 1338 passed; headless Vulkan with every dedicated route off:
24400 generic passes, 0 emulated draws, validation 0 errors / 0 SYNC-, the
state check reports no difference, frame matches the dedicated and emulated
routes within launch-to-launch variance (mean colour of repeated launches of
one route differs by more than the routes do).
… 2 and 3)

Every draw is native now. The device has no GL state machine, bound target,
texture-unit tables or emulated draws; GlStateTracker is gone and its shared
pipeline types live in Core/PipelineState.cs. The platform records what the
client states (StatedRenderState, including glUseProgram) and draws everything
without a dedicated route through Platform/StatedDraw.cs, keeping the scope open
so consecutive stated draws coalesce. Clears name their target and honour the
stated draw buffers, colour mask and depth mask. RenderTargetManager lost its
draw-buffer mask and sampled-slot exclusion; a declared pass's slots are the only
exclusion, with the frame graph on or off.

Fixes found on the way:
- native pipeline cache keyed on the described blend set, so draws differing
  only in dynamic blend/write mask no longer share one description
- pipeline formats for a pass ignore the target's current pass exclusion
- set-0 textures a draw does not name are made readable, or replaced by the
  placeholder (sky after the liquid depth pass: VUID-vkCmdDrawIndexed-imageLayout-00344)
- a platform framebuffer bind cancels an earlier fork bind
- a native pipeline request no longer counts a skipped draw

GPU tests keep their GL-shaped calls through a test-only GlShapedDevice that
records into StatedRenderState and draws through StatedDraw.

Verified: solution builds; Optimum.Tests 1333 passed; Optimum.Render.Vulkan.Tests
1082 passed (sync,best, implicit layers off); headless "serene cave world" on
Vulkan (0 validation errors, closed itself) and OpenGL (closed itself).
…n A)

The frame-marking half of the latency seams, ported from feat/latency without
its pacing backends. One latency frame id per rendered frame, allocated by the
pre-input LatencySleep lib seam (S3) or at BeginFrame; InputSample and
SimulationStart at the sleep, SimulationEnd and RenderSubmitStart at the first
render stage, RenderSubmitEnd after Submit A, PresentStart/PresentEnd around
vkQueuePresentKHR. Every frame submit is tagged through FrameSlot.Submit, every
present gets a process-wide present id, chained as VkPresentIdKHR where the
device supports it, and the swapchain tells the backend about each creation and
retirement. Device requirements go through IDeviceRequirementContributor and one
VkDeviceCreateInfo pNext chain. The stats sample gains stats.latency and the
latency_sleep wait site.

None is the only backend here: it never sleeps and owns no frame cap, so the
lib keeps its limiter and pacing is unchanged. Native, NV, AMD, LatencyMode and
OPTIMUM_VULKAN_LATENCY stay on feat/latency. Unlike there, VK_KHR_present_id is
enabled on its own merit, not only for NV. Device-side code is in
VulkanDevice.Latency.cs.

Verified: solution builds; make patch-il 224/224; Optimum.Tests latency,
integration and Cecil tests 109 passed; GPU latency, present identity, pacing
stats, swapchain and colour-write tests passed with sync,best validation and the
implicit layers off (RTX 4070, driver 615.71.09: present id ON, low_latency2
rev 2 detected only); headless "serene cave world" on Vulkan (0 validation
errors, stats.latency ~59 reports/s, sleep_n 0, closed itself) and OpenGL
(closed itself), frames identical by eye.
…on B)

The frame is rendered HUD-less and the UI is composed onto it at the end, always
on for Vulkan and with no switch: the structure an upscaler or a frame generator
needs, because the UI must never enter the image a reconstruction consumes.
OpenGL keeps drawing its GUI straight onto the window.

Slot 23 holds the HUD-less scene, copied from Primary colour 0 at the end of
RenderFinalComposition - the one moment the composited image holds the scene
alone. Slot 24 holds the UI image at window size, colour plus its own depth,
because the GUI depth-sorts itself over ScreenManager's 0..20000 range.

The scope opens at the end of BlitPrimaryToDefault, on every route out of it,
and the device resolves Default to the UI image while it is open
(VulkanDevice.RedirectDefaultFramebuffer), so no render system has to know it
moved - the atlas item renderer's LoadFrameBuffer(Default) included. Standard
blending into that image takes ONE as its source alpha factor
(AttachmentBlend.ForUiImage), so straight-alpha GUI accumulates the
over-operator's coverage instead of squaring it. The compose closes the scope
first and is the only draw that writes the window image after the blit;
BeginFrame and every framebuffer rebuild close a scope nobody composed.

The lib gains only neutral pieces: the OptimumComposeUiTarget virtual, its calls
in ClientMain.RenderToDefaultFramebuffer (before the Done stage, so the with-HUD
screenshot and the AVI writer still record the finished frame) and in
ScreenManager.Render (the menu screens never reach ClientMain), the ui-compose
program in both twins, and the two slot names for the parity dump.

Verified: solution builds; make patch-il 225/225; Optimum.Tests 1355 passed;
Optimum.Render.Vulkan.Tests 1132 passed with sync,best validation and the
implicit layers off; headless "serene cave world" on Vulkan (0 validation
errors) and OpenGL, both closed themselves; the parity dump at frame 300 shows
the snapshot, the UI colour and its coverage behaving as the over-operator, and
the composed frame matches what the GUI drawn directly gives.
@NightHammer1000

NightHammer1000 commented Sep 17, 2026

Copy link
Copy Markdown
Author

This approach fundamentally differs from the previous Vulkan implementation that was emulating OpenGL Calls.

Its the proper way to do this to actually get Vulkans full potential here.
Only Issue is that it cant support Mods that make GL Calls on their own.
So a Mod API surface is a future point to think about.

The UI separation and frame marking were backported from my DLSS and DLSS-FG branch as foundational systems here, as I will need them later on for upscaling, frame generation and VR

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.

1 participant