mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Check that a part's CPU inputs reach the GPU ahead of its own noted writes, and let a frame dump pick shaders and keep raw texels.
This pull request fixed lighting that added up frame after frame under shadow storage on Linux: a GPU write noted while a part was recorded kept that part's CPU inputs (the game's resets of its lighting accumulators) out of the shadow. Its fix marked noted ranges as submitted in vk_memory.c; the base copies that came with the black-world fix already move every CPU-changed byte in before the part runs, so that part is not taken and no SHADOW_PENDING_INPUTS setting is added. Taken from it: - tex_probe.c (--probe-vkmem): a range noted as a future GPU write still gets the CPU's bytes before the GPU reads it. The pull request's other check, that a CPU write made after a GPU result loses to it until the write-back, is left out: with base copies the later CPU write wins, as it does on the console. - vk_dump.c: NX2NAVI_DUMP_SHADERS=<hash>,... copies images only for passes that use those shaders, and NX2NAVI_DUMP_RAW=1 also saves each copied image's texel bytes. - tools/fix_linux_ambient_power.py, as it is: patches one translated shader of a player's own cache so a zero exponent no longer gives NaN. It holds two hashes and no shader data. Pull request #264 by intellq. The Windows files compile; the probe was not run, and nothing was run on Linux here. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ static const char *const known[] = {
|
||||
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CACHE_DIE", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COMPUTE_KEEP", "COLOR_CONTRAST", "COLOR_GAMMA",
|
||||
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
|
||||
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLSS_IDLE_FREE", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
|
||||
"DUMP_LOG", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
|
||||
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREE_CORES",
|
||||
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_BLOWOUT", "HDR_CONTRAST",
|
||||
"HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE",
|
||||
|
||||
@@ -348,6 +348,7 @@ static int test_import(pool_t *pool) {
|
||||
vk_mem_resolve(a + 0x1000, 0x20000, &sp);
|
||||
mem_written_pages(a, pool->size, pages, 64);
|
||||
vk_mem_gpu_pending(a + 0x1000, 0x20000);
|
||||
vk_mem_shadow_sync(); // as a submit does: the part's CPU inputs go in before its GPU work
|
||||
copy_wait(sb, 0, sp.buffer, sp.offset, 0x20000);
|
||||
vk_gpu_written(a + 0x1000, 0x20000);
|
||||
GCHECK(!memcmp(m + 0x1000, s, 0x20000), "GPU copy into the imported heap range differs from the source");
|
||||
@@ -362,10 +363,14 @@ static int test_import(pool_t *pool) {
|
||||
GCHECK(n == 4 && pages[0] == a + 0x30000 && pages[3] == a + 0x33000, "CPU writes to 4 pages: %llu reported",
|
||||
(unsigned long long)n);
|
||||
vk_mem_resolve(a + 0x30000, 0x4000, &sp);
|
||||
// A GPU write noted for the part being submitted must not keep the part's CPU inputs out: a buffer the shaders
|
||||
// add to and the CPU resets (the game's lighting accumulators) depends on the reset reaching the GPU first.
|
||||
vk_mem_gpu_pending(a + 0x30000, 0x4000);
|
||||
vk_mem_shadow_sync();
|
||||
copy_wait(sp.buffer, sp.offset, sb, 0, 0x4000);
|
||||
vmaInvalidateAllocation(c->vma, sa, 0, VK_WHOLE_SIZE);
|
||||
GCHECK(!memcmp(s, m + 0x30000, 0x4000), "GPU read of CPU-written heap differs");
|
||||
vk_gpu_written(a + 0x30000, 0x4000);
|
||||
vmaDestroyBuffer(c->vma, sb, sa);
|
||||
|
||||
// generations: written after `since` -> 1, then 0, explicit host write -> 1
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
// before the submission (vk_exec_draw.c upd_note).
|
||||
// NX2NAVI_DUMP_SKIP_RUNS=1: a render pass with the same targets as the render pass before it is logged but not copied
|
||||
// (the terrain decals draw one pass each into the G-buffer and would use up both caps).
|
||||
// NX2NAVI_DUMP_SHADERS=<hash>,...: copy images only for passes using those shader hashes; metadata covers the whole frame.
|
||||
// NX2NAVI_DUMP_RAW=1: also save each copied image's native texel bytes as .bin, for numerical analysis.
|
||||
#include <math.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
@@ -60,6 +62,8 @@ static uint64_t dump_after; // NX2NAVI_DUMP_AFTER
|
||||
static FILE *passes_f, *images_f;
|
||||
static int seq, finished, copied; // copied: passes and dispatches of the dump frame whose images were copied
|
||||
static int skip_runs, dump_ubo; // NX2NAVI_DUMP_SKIP_RUNS, NX2NAVI_DUMP_UBO (vec4 per block, 0 for none)
|
||||
static int dump_raw, ncopy_shaders;
|
||||
static uint64_t copy_shaders[16];
|
||||
static uint32_t want_tex[16][2]; // NX2NAVI_DUMP_TEX: slot id and 0, or width and height
|
||||
static int nwant_tex;
|
||||
static uint64_t last_targets; // targets_sig of the last render pass; 0 after a dispatch
|
||||
@@ -288,10 +292,10 @@ static void job_done(void *arg) {
|
||||
VmaAllocator vma = vk_ctx()->vma;
|
||||
vmaInvalidateAllocation(vma, j->alloc, 0, VK_WHOLE_SIZE);
|
||||
write_images(j);
|
||||
// A BC texture's raw blocks also go to <name>.bin, packed rows of blocks, so a script can decode them: the PNG
|
||||
// shows the block words through the integer mapping, which loses their bits.
|
||||
// Raw captures retain the exact texel bytes. BC blocks always get a .bin because the PNG's integer mapping
|
||||
// loses their bits; other formats get one when DUMP_RAW is enabled.
|
||||
size_t nl = strlen(j->name);
|
||||
if (nl > 7 && !strcmp(j->name + nl - 7, "_blocks")) {
|
||||
if (dump_raw || (nl > 7 && !strcmp(j->name + nl - 7, "_blocks"))) {
|
||||
char path[600];
|
||||
snprintf(path, sizeof path, "%s/%s.bin", dir, j->name);
|
||||
FILE *f = fopen(path, "wb");
|
||||
@@ -524,9 +528,10 @@ static void queue_lut(const VkTexImage *t, int id) {
|
||||
if (frame_now() != dump_frame) return;
|
||||
for (int i = 0; i < nluts_done; i++)
|
||||
if (luts_done[i] == t->image) return;
|
||||
if (nluts_done == MAX_LUTS) return;
|
||||
luts_done[nluts_done++] = t->image;
|
||||
uint32_t nl = t->type != VK_IMAGE_TYPE_3D && t->layers > 1 ? t->layers : 1; // a 2D array: one copy per layer
|
||||
for (int i = 0; i < nluts; i++)
|
||||
if (luts[i].image == t->image) return;
|
||||
if (nluts_done == MAX_LUTS || nluts == MAX_LUTS) return;
|
||||
uint32_t nl = t->type != VK_IMAGE_TYPE_3D && t->layers > 1 ? t->layers : 1;
|
||||
for (uint32_t k = 0; k < nl && nluts < MAX_LUTS; k++)
|
||||
luts[nluts++] = (Lut){t->image, t->format, t->width, t->height, t->type == VK_IMAGE_TYPE_3D ? t->depth : 1, id,
|
||||
t->type != VK_IMAGE_TYPE_3D, k, nl};
|
||||
@@ -559,6 +564,11 @@ static void flush_luts(VkExec *ex, int n) {
|
||||
: copy_out(ex, l->image, l->format, asp, 0, l->layer, l->w, l->h, l->d, name);
|
||||
fprintf(passes_f, " %s tex %d -> %s.png%s%s\n", l->is2d ? "2D" : "3D", l->id, name, why ? " skipped: " : "",
|
||||
why ? why : "");
|
||||
if (!why && nluts_done < MAX_LUTS) {
|
||||
int known = 0;
|
||||
for (int k = 0; k < nluts_done; k++) known |= luts_done[k] == l->image;
|
||||
if (!known) luts_done[nluts_done++] = l->image;
|
||||
}
|
||||
}
|
||||
nluts = 0;
|
||||
}
|
||||
@@ -909,9 +919,30 @@ static uint64_t targets_sig(const VkExec *ex) {
|
||||
}
|
||||
|
||||
// Sets pass_skip for the pass or dispatch being logged and counts it when it copies.
|
||||
static void pass_decide(int same_targets) {
|
||||
static int render_copy_selected(void) {
|
||||
if (!ncopy_shaders) return 1;
|
||||
for (int k = 0; k < ncopy_shaders; k++) {
|
||||
char vertex[24], fragment[24];
|
||||
snprintf(vertex, sizeof vertex, "vs %016llx", (unsigned long long)copy_shaders[k]);
|
||||
snprintf(fragment, sizeof fragment, "fs %016llx", (unsigned long long)copy_shaders[k]);
|
||||
for (int i = 0; i < nents; i++)
|
||||
if (strstr(ents[i].text, vertex) || strstr(ents[i].text, fragment)) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int compute_copy_selected(const GfxShader *cs) {
|
||||
if (!ncopy_shaders) return 1;
|
||||
uint64_t h = shader_hash(cs);
|
||||
for (int k = 0; k < ncopy_shaders; k++)
|
||||
if (copy_shaders[k] == h) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void pass_decide(int same_targets, int selected) {
|
||||
if (frame_now() != dump_frame) copied = 0;
|
||||
pass_skip = copied >= MAX_PASSES ? "pass cap" : same_targets ? "same targets as the previous pass" : NULL;
|
||||
pass_skip = !selected ? "shader not selected" : copied >= MAX_PASSES ? "pass cap" :
|
||||
same_targets ? "same targets as the previous pass" : NULL;
|
||||
if (!pass_skip) copied++;
|
||||
}
|
||||
|
||||
@@ -928,7 +959,7 @@ void vk_dump_pass_end(VkExec *ex) {
|
||||
if (!active()) goto out;
|
||||
int n = seq++;
|
||||
uint64_t sig = targets_sig(ex);
|
||||
pass_decide(skip_runs && sig == last_targets);
|
||||
pass_decide(skip_runs && sig == last_targets, render_copy_selected());
|
||||
last_targets = sig;
|
||||
fprintf(passes_f, "pass %04d: render pass, draws %d, area %ux%u layers %u, fence %llu, frame %llu\n", n, pass_draws,
|
||||
ex->pass_w, ex->pass_h, ex->pass_layers, (unsigned long long)ex->fence, (unsigned long long)frame_now());
|
||||
@@ -958,7 +989,7 @@ void vk_dump_dispatch(VkExec *ex, const GfxShader *cs, VkPipeline p, const VkPip
|
||||
plat_mutex_lock(&rec_lock);
|
||||
if (!active()) goto out;
|
||||
int n = seq++;
|
||||
pass_decide(0);
|
||||
pass_decide(0, compute_copy_selected(cs));
|
||||
last_targets = 0;
|
||||
Str b = {0};
|
||||
describe_bindings(&b, ex, info);
|
||||
@@ -1143,6 +1174,16 @@ void vk_dump_init(void) {
|
||||
if (dump_ubo == 1) dump_ubo = 16;
|
||||
if (dump_ubo > 4096) dump_ubo = 4096;
|
||||
if (dump_ubo < 0) dump_ubo = 0;
|
||||
const char *raw = cfg_get("NX2NAVI_DUMP_RAW");
|
||||
dump_raw = raw && *raw && *raw != '0';
|
||||
const char *shaders = cfg_get("NX2NAVI_DUMP_SHADERS");
|
||||
for (const char *p = shaders; p && *p && ncopy_shaders < 16;) {
|
||||
char *end;
|
||||
uint64_t h = strtoull(p, &end, 16);
|
||||
if (end == p) break;
|
||||
copy_shaders[ncopy_shaders++] = h;
|
||||
p = *end ? end + 1 : end;
|
||||
}
|
||||
const char *wt = cfg_get("NX2NAVI_DUMP_TEX");
|
||||
for (char *q = (char *)wt; q && *q && nwant_tex < 16; q += *q == ',') {
|
||||
uint32_t *w = want_tex[nwant_tex++];
|
||||
|
||||
89
tools/fix_linux_ambient_power.py
Normal file
89
tools/fix_linux_ambient_power.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the verified Ocarina 1.1.1 ambient-power workaround to a Linux shader cache file.
|
||||
|
||||
The affected shader evaluates exp2(log2(height) * exponent). A zero height
|
||||
and zero exponent produce a NaN, which then contaminates ambient lighting
|
||||
and volumetric history. Select a zero product when the exponent is zero.
|
||||
This is a narrowly scoped compatibility workaround, not a claim of exact
|
||||
NVIDIA instruction equivalence. It does not address exposure or memory use.
|
||||
No game shader data is embedded in this tool.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ORIGINAL_HASH = 0xA806B657C39684BD
|
||||
PATCHED_HASH = 0x0A0281063D5EF5BD
|
||||
|
||||
|
||||
def shader_hash(data):
|
||||
value = 0xCBF29CE484222325
|
||||
for byte in data:
|
||||
value = ((value ^ byte) * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF
|
||||
return value
|
||||
|
||||
|
||||
def patch_text(source):
|
||||
pattern = re.compile(
|
||||
r'(?P<ptr>%\w+) = OpAccessChain %\w+ %cp_c3 %int_0 %int_2 %int_1\s+'
|
||||
r'(?P<exponent>%\w+) = OpLoad %float (?P=ptr)\s+'
|
||||
r'(?P<product>%\w+) = OpFMul %float (?P<logarithm>%\w+) (?P=exponent)\s+'
|
||||
r'(?P<result>%\w+) = OpExtInst %float %\w+ Exp2 (?P=product)'
|
||||
)
|
||||
matches = list(pattern.finditer(source))
|
||||
if len(matches) != 1:
|
||||
raise ValueError('Expected exactly one supported ambient-power expression')
|
||||
m = matches[0]
|
||||
old = f"{m['product']} = OpFMul %float {m['logarithm']} {m['exponent']}"
|
||||
replacement = (
|
||||
f"%linux_pow_product = OpFMul %float {m['logarithm']} {m['exponent']}\n"
|
||||
f"%linux_pow_zero_exponent = OpFOrdEqual %bool {m['exponent']} %float_0\n"
|
||||
f"{m['product']} = OpSelect %float %linux_pow_zero_exponent %float_0 %linux_pow_product"
|
||||
)
|
||||
if source.count(old) != 1:
|
||||
raise ValueError('The supported multiply instruction is not unique')
|
||||
return source.replace(old, replacement)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('shader', type=Path, help='Supported .spv shader cache file')
|
||||
destination = parser.add_mutually_exclusive_group(required=True)
|
||||
destination.add_argument('--output', type=Path)
|
||||
destination.add_argument('--in-place', action='store_true')
|
||||
args = parser.parse_args()
|
||||
original = args.shader.read_bytes()
|
||||
current = shader_hash(original)
|
||||
if current == PATCHED_HASH:
|
||||
if args.output:
|
||||
args.output.write_bytes(original)
|
||||
print(f'Already patched: {current:016x}')
|
||||
return
|
||||
if current != ORIGINAL_HASH:
|
||||
parser.error(f'Unsupported shader hash {current:016x}; file left unchanged')
|
||||
for command in ('spirv-dis', 'spirv-as', 'spirv-val'):
|
||||
if not shutil.which(command):
|
||||
parser.error(f'Required command not found: {command}')
|
||||
target = args.shader if args.in_place else args.output
|
||||
with tempfile.TemporaryDirectory(prefix='.ambient-power-', dir=target.parent) as work:
|
||||
work = Path(work)
|
||||
assembly = work / 'shader.spvasm'
|
||||
binary = work / 'shader.spv'
|
||||
subprocess.run(['spirv-dis', str(args.shader), '-o', str(assembly)], check=True)
|
||||
assembly.write_text(patch_text(assembly.read_text()))
|
||||
subprocess.run(['spirv-as', '--target-env', 'vulkan1.1', str(assembly), '-o', str(binary)], check=True)
|
||||
subprocess.run(['spirv-val', '--target-env', 'vulkan1.1', str(binary)], check=True)
|
||||
patched = binary.read_bytes()
|
||||
result = shader_hash(patched)
|
||||
if result != PATCHED_HASH:
|
||||
raise ValueError(f'Unexpected assembled shader hash {result:016x}; file left unchanged')
|
||||
binary.chmod(args.shader.stat().st_mode & 0o777)
|
||||
binary.replace(target)
|
||||
print(f'Patched {target}: {current:016x} -> {result:016x}; Vulkan 1.1 validation passed')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user