diff --git a/runtime/cfg.c b/runtime/cfg.c index 1bcd1cf4..d8ad7e69 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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", diff --git a/runtime/tex_probe.c b/runtime/tex_probe.c index 8fb56bdb..381f2233 100644 --- a/runtime/tex_probe.c +++ b/runtime/tex_probe.c @@ -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 diff --git a/runtime/vk_dump.c b/runtime/vk_dump.c index a3bd7678..0835c482 100644 --- a/runtime/vk_dump.c +++ b/runtime/vk_dump.c @@ -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=,...: 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 #include #include @@ -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 .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++]; diff --git a/tools/fix_linux_ambient_power.py b/tools/fix_linux_ambient_power.py new file mode 100644 index 00000000..d2e4084a --- /dev/null +++ b/tools/fix_linux_ambient_power.py @@ -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%\w+) = OpAccessChain %\w+ %cp_c3 %int_0 %int_2 %int_1\s+' + r'(?P%\w+) = OpLoad %float (?P=ptr)\s+' + r'(?P%\w+) = OpFMul %float (?P%\w+) (?P=exponent)\s+' + r'(?P%\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()