Files
d4r/kernels/native/split_output_paths.py
T
ayo 162bbd77c8 shim: adaptive marker polling, opt-in GPU-side input wait/completion and RGBA8 output
- [Interop] MarkerPollUs = auto is the new default: the input-marker wait learns when
  each feature's inputs become ready, sleeps in <=200 us chunks, yields near the
  prediction and backs off when late (d4r_input_wait.h); a number keeps fixed pacing
  (0 yields). d4r_config.py no longer exports 200 when the key is unset

- D4R_SHIM_GPU_INPUT_WAIT=1 (experimental, split VRAM, age 0): NGX's null stream
  waits on a coherent Vulkan word imported into HIP (d4rStreamWaitValue32Eq) instead
  of CPU polling. The original readback marker stays the authority; a CPU watchdog
  releases discarded lists and timeouts. Denoiser, verify, capture and dilation
  frames keep the CPU wait

- D4R_SHIM_GPU_COMPLETION=1 (needs GPU_INPUT_WAIT): the bridge writes a completion
  word right after the native output kernel (d4rNgxWriteValue32), and the game's queue
  polls it with a bounded compute wait (kernels/support/vulkan_wait.comp), replacing
  the split list's CPU timeline signal. frame_retired writes the same word on every
  failure path

- D4R_SHIM_NATIVE_COMPLETION=1: a null-stream callback in the bridge signals the
  split timeline (d4rNgxTimeline*); D4rFrameCompletion::can_signal only releases the
  oldest outstanding frame

- RGBA8 output: tex_common.h adds an 'R4' redirect tag, so K's native output kernel
  stores packed RGBA8 directly (D4R_SHIM_OUTPUT_RGBA8=1, checked against the kernel
  manifest by d4rOutputKernelRgba8). Host packing handles RGBA8 too.
  patches/vkd3d-proton/0004 (VKD3D_D4R_LINEAR_RGBA8=WxH) makes such outputs linear and
  exportable so the result can be written in place

- D4R_SHIM_FUSE_RGBA8_INPUT=1: one Vulkan pass converts and copies RGBA8 color input
  (vulkan_read.comp mode 5 matches the blit's toward-zero halves, plus a row stride)

- profiling: thread CPU time from the Unix bridge (Wine's GetThreadTimes ticks at
  10 ms); game-queue D3D12 timestamps (vk_input/vk_dlss_wait/vk_output/vk_total);
  gpu_eval split into input_gpu_wait and dlss_gpu; D4R_PROFILE_GPU_EVENTS,
  _TIMESTAMPS, _LOG and _CPU_TIME=0 switch off individual parts for bisection

- bridge: D4R_NGX_STREAM_PRIORITY=high|normal runs NGX on a priority stream
  (off by default; no gain measured)

- release_feature: a feature already marked retiring by a failed drain can still
  be released normally

- harness: RGBA8 with motion and jitter scenes (re-quantized each frame),
  D4R_HARNESS_RGBA8_RAMP, D4R_HARNESS_THRASH_MB to evict L2/Infinity Cache before
  each evaluation

- kernels/native: in_hoist.py and split_output_paths.py (opt-in scheduling
  experiments); scripts/build_k_ldr_output.sh builds K's LDR output with an
  optional D4R_LDR_OUTPUT_MAX_VGPR cap

- tests: test_input_wait.py and test_latency_optimizations.py; MarkerPollUs and
  frame-completion tests updated
2026-10-13 22:30:41 -04:00

72 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Split a translated hiluma output kernel into its two per-block paths (opt-in experiment).
usage: split_output_paths.py KERNEL.hip > KERNEL_split.hip
NVIDIA's hiluma_engine_output_* kernels pick one of two code paths per block: path B (an LDS tile of the
block's render footprint) when the footprint is under 24 texels in both axes, which holds for every block at
Quality .. Ultra Performance, else path A (direct fetches, DLAA). The choice depends only on the block index and
kernel parameters, so it is uniform per block, and blocks share nothing. Path A needs more registers than path B
(LDR mvlo: 101 vs 88 VGPRs), and one kernel carries the larger allocation for every block.
This pass emits the same translated body twice:
NAME runs path B and returns where path A would start;
NAME_post1 runs path A and returns where path B would start.
The ZLUDA/MicroCUDA native hook launches NAME_post1 right after NAME on the same stream with the same parameters
and grid (d4r_post1_grid_x/_y = 0), with the 16x16 blocks the PTX hard-codes (tid.y << 4). Each block executes
exactly the instructions it executed before; only the block order across the two paths changes, which no output
depends on (blocks never read what other blocks write in the same launch).
The pass refuses anything but the expected entry structure:
pA = ((int32_t)rX < (int32_t)24); pB = ((int32_t)rY < (int32_t)24); pC = (pA && pB);
if (pC) goto LB;
goto LA;
"""
import re
import sys
def split(source):
head, sep, main = source.partition('extern "C" __attribute__((global))')
if not sep or 'extern "C" __attribute__((global))' in main:
raise SystemExit('expected exactly one kernel')
m = re.match(r'\s*__attribute__\(\(amdgpu_flat_work_group_size\(1, 256\)\)\) void (\w+)\((\w+)\)', main)
if not m:
raise SystemExit('unexpected kernel signature')
name = m.group(1)
pattern = re.compile(r'^ (p\d+) = \(\(int32_t\)r\d+ < \(int32_t\)24\);\n'
r' (p\d+) = \(\(int32_t\)r\d+ < \(int32_t\)24\);\n'
r' (p\d+) = \((p\d+) && (p\d+)\);\n'
r' if \((p\d+)\) goto (L__BB0_\d+);\n'
r' goto (L__BB0_\d+);\n', re.M)
found = list(pattern.finditer(main))
if len(found) != 1:
raise SystemExit(f'path selection found {len(found)} times, expected once')
g = found[0]
pa, pb, pc, x, y, pc2, path_b, path_a = g.groups()
if {x, y} != {pa, pb} or pc != pc2:
raise SystemExit('path selection predicate has an unexpected form')
# the predicate must not be reassigned anywhere (it is the block's path choice)
if len(re.findall(rf'^ {pc} = ', main, re.M)) != 1:
raise SystemExit(f'{pc} is assigned more than once')
sel = g.group(0)
lines = sel.splitlines(keepends=True)
main_b = main.replace(sel, ''.join(lines[:4]) + ' return; // path A blocks: NAME_post1\n', 1)
main_a = main.replace(sel, ''.join(lines[:3]) + f' if ({pc}) return; // path B blocks: NAME\n'
+ f' goto {path_a};\n', 1)
main_a = main_a.replace(f' void {name}(', f' void {name}_post1(', 1)
globals_ = ('\n// d4r native hook: launch NAME_post1 after NAME with the launch grid and the PTX\'s 16x16 blocks\n'
'extern "C" __attribute__((device, used)) uint32_t d4r_post1_grid_x = 0;\n'
'extern "C" __attribute__((device, used)) uint32_t d4r_post1_grid_y = 0;\n'
'extern "C" __attribute__((device, used)) uint32_t d4r_post1_block_x = 16;\n'
'extern "C" __attribute__((device, used)) uint32_t d4r_post1_block_y = 16;\n'
'extern "C" __attribute__((device, used)) uint32_t d4r_post1_block_z = 1;\n')
banner = '// split_output_paths.py: NAME = path B only, NAME_post1 = path A only (per-block choice unchanged)\n'
return (banner + head + globals_ + sep + main_b + '\n' + sep + main_a, path_b, path_a)
if __name__ == '__main__':
text, b, a = split(open(sys.argv[1]).read())
sys.stderr.write(f'split: path B {b}, path A {a}\n')
sys.stdout.write(text)