rr: add FSR Ray Regeneration, exact RDNA3 inference and motion experiments
Add an experimental FSR Ray Regeneration backend for D3D12 RR requests: - Convert depth, motion, normals and albedos into AMD denoiser inputs. - Split and recompose diffuse/specular signals, then run Super Sampling. - Add stability/detail controls, demodulation guards and halo limiting. - Handle AMD provider discovery under NVIDIA GPU spoofing. - Add runtime probes, synthetic reference scenes and per-dispatch profiling. Add exact gfx11 DLSS-RR inference using f16 cooperative matrices: - Reproduce FP8 conversions, accumulation and camera reprojection arithmetic. - Widen model weights and avoid requiring VK_EXT_shader_float8. - Extend model building, replay tools and descriptor metadata. Expand motion preprocessing and experimental AW2 native-motion support: - Add tiled LDS dilation, padded allocation handling and thin-only sky fill. - Order dilation with input waits and NGX on the selected stream. - Add jitter-aligned native composition, hybrid routing and wire history repair. - Add prepared masks, sparse repair and independent motion/repair controls. Add optional fused R11G11B10 input/output conversion, sampled stage timings and profile summarization. Extend configuration, packaging, documentation, FidelityFX headers and CPU/GPU regression coverage.
This commit is contained in:
1 parent
17c1828f97
commit
34e6b0fe71
83 files changed
+11300
-195
No files matched your search
@@ -174,6 +174,17 @@ int main(int argc, char** argv) {
|
||||
self.config("[Interop]\nMotionVectorDilation = 2\n",
|
||||
{"D4R_MOTION_DILATION": "0"}).stdout)
|
||||
|
||||
def test_thin_sky_opt_in_and_environment_precedence(self):
|
||||
for value, expected in (("false","0"),("true","1"),("auto","0")):
|
||||
self.assertIn(f"export D4R_MOTION_THIN_SKY={expected}",
|
||||
self.config(f"[Interop]\nMotionDilationThinSky = {value}\n").stdout)
|
||||
self.config("[Interop]\nMotionDilationThinSky = maybe\n", expected=2)
|
||||
self.assertNotIn("export D4R_MOTION_THIN_SKY",
|
||||
self.config("[Interop]\nMotionDilationThinSky = true\n",
|
||||
{"D4R_MOTION_THIN_SKY":"0"}).stdout)
|
||||
for file in ("config/d4r.ini.default", "packaging/d4r.ini"):
|
||||
self.assertRegex((ROOT/file).read_text(), r"(?m)^MotionDilationThinSky\s*=\s*false$")
|
||||
|
||||
def test_shipped_configs_default_off(self):
|
||||
for file in ("config/d4r.ini.default", "packaging/d4r.ini"):
|
||||
self.assertRegex((ROOT / file).read_text(), r"(?m)^PreferAccuracy\s*=\s*false$")
|
||||
|
||||
@@ -185,11 +185,13 @@ int wmain(int argc, wchar_t** argv) {
|
||||
(runtime / 'ngx').mkdir(parents=True)
|
||||
for name in ('nvngx.dll', 'd4r.ini'):
|
||||
(runtime / name).touch()
|
||||
dlss, ngx = b'verified DLSS fixture', b'verified NGX fixture'
|
||||
dlss, dlssd, ngx = b'verified DLSS fixture', b'verified DLSS RR fixture', b'verified NGX fixture'
|
||||
(runtime / 'nvngx_dlss.dll').write_bytes(dlss)
|
||||
(runtime / 'nvngx_dlssd.dll').write_bytes(dlssd)
|
||||
(runtime / 'ngx/_nvngx.dll').write_bytes(ngx)
|
||||
script = (ROOT / 'install.sh').read_text()
|
||||
replacements = {'DLSS_SHA256': hashlib.sha256(dlss).hexdigest(),
|
||||
'DLSSD_SHA256': hashlib.sha256(dlssd).hexdigest(),
|
||||
'NGX_SHA256': hashlib.sha256(ngx).hexdigest()}
|
||||
script = '\n'.join(key + '=' + replacements[key] if key in replacements else line
|
||||
for line in script.splitlines() for key in [line.split('=', 1)[0]]) + '\n'
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Exercise the actual optional output recorder's lifetime and fallback paths."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class FusedVramOutputTests(unittest.TestCase):
|
||||
def test_output_lifetime_and_fallback(self):
|
||||
shim=(ROOT/'tools/d4r_nvngx_shim.cpp').read_text()
|
||||
start=shim.index('static bool record_feature_vram_output(')
|
||||
end=shim.index('// --- Split frames',start)
|
||||
input_start=shim.index('static bool ensure_fused_color(')
|
||||
input_end=shim.index('// Records the input copies',input_start)
|
||||
program=r'''
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
using VkImage=uint64_t;using VkImageView=uint64_t;using VkBuffer=uint64_t;
|
||||
using VkCommandBuffer=uint64_t;using D3D12_RESOURCE_STATES=int;
|
||||
constexpr int VK_SUCCESS=0,VK_NULL_HANDLE=0,VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO=1,
|
||||
VK_STRUCTURE_TYPE_MEMORY_BARRIER=2,VK_IMAGE_VIEW_TYPE_2D=3,VK_FORMAT_B10G11R11_UFLOAT_PACK32=4,
|
||||
VK_IMAGE_ASPECT_COLOR_BIT=1,VK_ACCESS_MEMORY_WRITE_BIT=1,VK_ACCESS_SHADER_READ_BIT=2,
|
||||
VK_ACCESS_SHADER_WRITE_BIT=4,VK_ACCESS_MEMORY_READ_BIT=8,VK_PIPELINE_STAGE_ALL_COMMANDS_BIT=1,
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT=2,DXGI_FORMAT_R11G11B10_FLOAT=26,
|
||||
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS=4,D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE=8,
|
||||
DXGI_FORMAT_R8G8B8A8_UNORM=28,VK_FORMAT_R8G8B8A8_UNORM=5,D3D12_RESOURCE_STATE_UNORDERED_ACCESS=8,
|
||||
D3D12_RESOURCE_STATE_COPY_DEST=16;
|
||||
#define SUCCEEDED(value) ((value)>=0)
|
||||
struct Range {int aspect,baseMip,levels,baseLayer,layers;};
|
||||
struct VkImageViewCreateInfo {int sType;VkImage image=0;int viewType=0,format=0;Range subresourceRange={};};
|
||||
struct VkMemoryBarrier {int sType;void* next;int src,dst;};
|
||||
struct Desc {int Format=26,Flags=4;};
|
||||
struct ID3D12Resource {Desc desc;int refs=0;Desc GetDesc(){return desc;}void AddRef(){++refs;}};
|
||||
struct ID3D12GraphicsCommandList {};
|
||||
struct Set {int pool=0;};
|
||||
struct FusedColorInput {VkImage image=0;VkImageView view=0;VkBuffer buffer=0;Set set;ID3D12Resource* owner=nullptr;uint32_t inputMode=5;};
|
||||
struct VramBuffer {VkBuffer buffer=1;size_t bytes=800;};
|
||||
struct VramCopy {bool convert=true;unsigned width=10,height=10;VkImage image=10;};
|
||||
struct Feature {bool fusedOutputAllowed=true,recordOutputFused=false,fusedColorAllowed=true;
|
||||
struct {VramBuffer vram[1];struct {size_t size(){return 800;}} host[1];} inputs[3];
|
||||
FusedColorInput fusedColor[3];std::vector<FusedColorInput> retiredFusedColor;struct {VramBuffer vram;} outputs[4];FusedColorInput fusedOutput[4];std::vector<FusedColorInput> retiredFusedOutput;int outputConversion=0;};
|
||||
static unsigned enabled=1;
|
||||
static bool g_fusedColorReady=true,viewOk=true,setOk=true,beginOk=true,endOk=true;
|
||||
static int views=0,destroyed=0,frees=0,records=0,legacy=0;
|
||||
static unsigned env_uint(const char*,unsigned){return enabled;}
|
||||
static void logf(const char*,...) {}
|
||||
static std::vector<int> transitions;
|
||||
static void transition(ID3D12GraphicsCommandList*,ID3D12Resource*,int,int after){transitions.push_back(after);}
|
||||
static bool record_vram_output(ID3D12GraphicsCommandList*,const VramBuffer&,const VramCopy&,int){++legacy;return true;}
|
||||
struct Interop {int BeginVkCommandBufferInterop(ID3D12GraphicsCommandList*,VkCommandBuffer* c){*c=1;return beginOk?0:-1;}int EndVkCommandBufferInterop(ID3D12GraphicsCommandList*){return endOk?0:-1;}} interop;
|
||||
static struct {
|
||||
int device=0;Interop* interop=&::interop;
|
||||
int createView(int,const VkImageViewCreateInfo*,void*,VkImageView* out){++views;if(viewOk)*out=5;return viewOk?0:-1;}
|
||||
void destroyView(int,VkImageView,void*){++destroyed;}
|
||||
void barrier(VkCommandBuffer,int,int,int,int,const VkMemoryBarrier*,int,void*,int,void*){}
|
||||
} g_vk;
|
||||
static struct {
|
||||
bool set(Set& s,VkImageView,int,VkBuffer,size_t,bool){s.pool=1;return setOk;}
|
||||
void free(Set& s){++frees;s={};}
|
||||
void record(VkCommandBuffer,const Set&,unsigned,unsigned){++records;}
|
||||
} g_fusedColorStage;
|
||||
''' + shim[input_start:input_end] + shim[start:end] + r'''
|
||||
int main() {
|
||||
ID3D12GraphicsCommandList list;ID3D12Resource output,changed;VramCopy copy;Feature f;
|
||||
enabled=false;assert(record_feature_vram_output(f,0,&list,&output,8,copy));assert(legacy==1 && views==0 && records==0);
|
||||
enabled=true;assert(record_feature_vram_output(f,0,&list,&output,8,copy));assert(output.refs==1 && views==1 && records==1 && legacy==1);
|
||||
assert(record_feature_vram_output(f,0,&list,&output,8,copy));assert(output.refs==1 && views==1 && records==2);
|
||||
assert(record_feature_vram_output(f,0,&list,&changed,8,copy));assert(changed.refs==1 && output.refs==1 && f.retiredFusedOutput.size()==1);
|
||||
Feature missingUav;ID3D12Resource noUav;noUav.desc.Flags=0;int old=legacy;
|
||||
assert(record_feature_vram_output(missingUav,0,&list,&noUav,8,copy));assert(legacy==old+1 && !noUav.refs);
|
||||
Feature small;small.outputs[0].vram.bytes=799;old=legacy;
|
||||
assert(record_feature_vram_output(small,0,&list,&output,8,copy));assert(legacy==old+1);
|
||||
Feature packed;copy.convert=false;old=legacy;
|
||||
assert(record_feature_vram_output(packed,0,&list,&output,8,copy));assert(legacy==old+1);copy.convert=true;
|
||||
Feature noView;viewOk=false;old=legacy;
|
||||
assert(record_feature_vram_output(noView,0,&list,&output,8,copy));assert(legacy==old+1 && !noView.fusedOutputAllowed);viewOk=true;
|
||||
Feature noSet;setOk=false;int released=destroyed;old=legacy;
|
||||
assert(record_feature_vram_output(noSet,0,&list,&output,8,copy));assert(legacy==old+1 && destroyed==released+1 && !noSet.fusedOutputAllowed);setOk=true;
|
||||
Feature noBegin;beginOk=false;old=legacy;int oldRecords=records;
|
||||
assert(record_feature_vram_output(noBegin,0,&list,&output,8,copy));assert(legacy==old+1 && records==oldRecords && !noBegin.fusedOutputAllowed);beginOk=true;
|
||||
Feature noEnd;endOk=false;old=legacy;
|
||||
assert(record_feature_vram_output(noEnd,0,&list,&output,8,copy));assert(legacy==old+1 && !noEnd.fusedOutputAllowed);endOk=true;
|
||||
Feature bounded;bounded.retiredFusedOutput.resize(64);old=legacy;int oldViews=views;
|
||||
assert(record_feature_vram_output(bounded,0,&list,&output,8,copy));assert(legacy==old+1 && views==oldViews && !bounded.fusedOutputAllowed);
|
||||
enabled=2;Feature alternating;old=legacy;
|
||||
assert(record_feature_vram_output(alternating,0,&list,&output,8,copy,0));assert(legacy==old+1 && !alternating.recordOutputFused);
|
||||
assert(record_feature_vram_output(alternating,0,&list,&output,8,copy,300));assert(legacy==old+1 && alternating.recordOutputFused);
|
||||
assert(record_feature_vram_output(alternating,0,&list,&output,8,copy,600));assert(legacy==old+2 && !alternating.recordOutputFused);
|
||||
enabled=1;Feature input;ID3D12Resource r11;
|
||||
assert(ensure_fused_color(input,0,&r11,copy,0));assert(input.fusedColor[0].inputMode==1 && r11.refs==1);
|
||||
int inputViews=views;assert(ensure_fused_color(input,0,&r11,copy,0));assert(views==inputViews && r11.refs==1);
|
||||
enabled=2;assert(!ensure_fused_color(input,0,&r11,copy,300));assert(ensure_fused_color(input,0,&r11,copy,600));
|
||||
assert(!ensure_fused_color(input,0,&r11,copy,1200));assert(views==inputViews);
|
||||
enabled=1;ID3D12Resource rgba8;rgba8.desc.Format=28;Feature rgbaInput;
|
||||
assert(ensure_fused_color(rgbaInput,0,&rgba8,copy,0));assert(rgbaInput.fusedColor[0].inputMode==5);
|
||||
ID3D12Resource denied;denied.desc.Flags=8;assert(!ensure_fused_color(input,0,&denied,copy));
|
||||
Feature limitedInput;limitedInput.retiredFusedColor.resize(64);
|
||||
assert(!ensure_fused_color(limitedInput,0,&r11,copy) && !limitedInput.fusedColorAllowed);
|
||||
assert(transitions.back()==8); // every route restores caller's output state
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
runner=Path(temp)/'fused-output'
|
||||
subprocess.run(['c++','-std=c++17','-Wall','-Wextra','-Werror','-Wno-missing-field-initializers',
|
||||
'-x','c++','-','-o',str(runner)],input=program,text=True,check=True,capture_output=True)
|
||||
subprocess.run([str(runner)],check=True,timeout=5)
|
||||
@@ -12,6 +12,7 @@ class MotionGeometryTests(unittest.TestCase):
|
||||
source = r'''
|
||||
#include "d4r_motion_dilation.h"
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
int main() {
|
||||
D4rMotionDilationParams p = {};
|
||||
p.render_width = 854; p.render_height = 480;
|
||||
@@ -21,6 +22,18 @@ int main() {
|
||||
assert(d4r_motion_dilation_geometry(&low, 0xb, 2560, 1440));
|
||||
assert(low.motion_width == 854 && low.motion_height == 480);
|
||||
assert(low.motion_pitch == 4096 && low.jitter_x == 0 && low.jitter_y == 0);
|
||||
for (unsigned radius : {1u, 2u}) {
|
||||
low.radius = radius;
|
||||
assert(d4r_motion_dilation_lds_eligible(&low));
|
||||
auto fallback = low; fallback.jitter_x = 0.25f;
|
||||
assert(!d4r_motion_dilation_lds_eligible(&fallback));
|
||||
fallback = low; ++fallback.motion_height;
|
||||
assert(!d4r_motion_dilation_lds_eligible(&fallback));
|
||||
fallback = low; ++fallback.motion_width;
|
||||
assert(!d4r_motion_dilation_lds_eligible(&fallback));
|
||||
}
|
||||
low.radius = 0; assert(!d4r_motion_dilation_lds_eligible(&low));
|
||||
low.radius = 3; assert(!d4r_motion_dilation_lds_eligible(&low));
|
||||
low = p;
|
||||
assert(d4r_motion_dilation_geometry(&low, 0x4b, 2560, 1440));
|
||||
low = p; low.motion_width = 853;
|
||||
|
||||
@@ -121,3 +121,152 @@ class MotionKernelGpuTests(unittest.TestCase):
|
||||
_, sx, sy = candidates[0]
|
||||
expected[y*stride+x] = motion[sy*stride+sx]
|
||||
self.assertEqual((output/'alloc-2.bin').read_bytes(), struct.pack(f'<{len(expected)}I', *expected))
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS') == '1', 'opt-in GPU test')
|
||||
class MotionLdsKernelGpuTests(unittest.TestCase):
|
||||
def run_kernel(self, name, width, height, stride, allocation_height, depth, motion, radius, reverse):
|
||||
runner = os.environ['D4R_DUMP_RUNNER']
|
||||
kernel = ROOT / 'build/wine-nvcuda/generated' / (os.environ.get('D4R_GPU_ARCH', 'gfx1101') + '.hsaco')
|
||||
# Guard the tail beyond the allocation too. Generic output is pre-copied,
|
||||
# matching the bridge's original full-plane copy followed by dilation.
|
||||
guard = [0xA5A5A5A5] * 32
|
||||
original = motion + guard
|
||||
arrays = [struct.pack(f'<{len(depth)}f', *depth),
|
||||
struct.pack(f'<{len(original)}I', *original),
|
||||
struct.pack(f'<{len(original)}I', *(original if name == 'd4r_motion_dilate' else [0xA5A5A5A5]*len(original)))]
|
||||
addresses = (0x100000000, 0x200000000, 0x300000000)
|
||||
args = struct.pack('<5Q4I2f2I', *addresses, stride*4, stride*4,
|
||||
width, height, width, height, 0., 0., radius, reverse)
|
||||
if name.startswith('d4r_motion_dilate_lds'):
|
||||
args += struct.pack('<I', allocation_height)
|
||||
grid = f'{(stride+15)//16} {(allocation_height+15)//16} 1 256 1 1 0'
|
||||
else:
|
||||
grid = f'{(width*height+127)//128} 1 1 128 1 1 0'
|
||||
with tempfile.TemporaryDirectory() as t:
|
||||
directory = Path(t); output = directory/'out'; output.mkdir()
|
||||
(directory/'args.bin').write_bytes(args)
|
||||
manifest = [f'kernel {name}', f'launch {grid}', f'args {len(args)}']
|
||||
for i, data in enumerate(arrays):
|
||||
(directory/f'alloc-{i}.bin').write_bytes(data)
|
||||
manifest += [f'alloc {i} {hex(addresses[i])} {len(data)}', f'pointer {i*8} {i} 0']
|
||||
(directory/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
subprocess.run([runner, str(kernel), name, str(directory), str(output), '1'],
|
||||
check=True, capture_output=True, timeout=30)
|
||||
for i in (0, 1):
|
||||
self.assertEqual((output/f'alloc-{i}.bin').read_bytes(), arrays[i], 'input mutated')
|
||||
result = (output/'alloc-2.bin').read_bytes()
|
||||
self.assertEqual(result[-128:], struct.pack('<32I', *guard), 'tail guard overwritten')
|
||||
values = struct.unpack(f'<{len(original)}I', result)
|
||||
for y in range(allocation_height):
|
||||
for x in range(stride):
|
||||
if x >= width or y >= height:
|
||||
self.assertEqual(values[y*stride+x], motion[y*stride+x], 'padding changed')
|
||||
return result
|
||||
|
||||
def test_image_edges_block_edges_and_lexicographic_sky_ties(self):
|
||||
width, height, stride, allocation_height = 35, 33, 48, 36
|
||||
# Equal-distance sources straddle x=16/y=16 tile boundaries. Include
|
||||
# corners, circle boundary, and same-x/different-y tie candidates.
|
||||
source_sets = [
|
||||
[(0, 0), (34, 32), (13, 16), (19, 16), (8, 31)],
|
||||
[(0, 0), (34, 32), (16, 13), (16, 19), (8, 31)],
|
||||
]
|
||||
for sources in source_sets:
|
||||
for reverse in (False, True):
|
||||
for radius in (1, 2):
|
||||
with self.subTest(reverse=reverse, radius=radius, sources=sources):
|
||||
depth = [float('nan')] * (stride*allocation_height)
|
||||
motion = list(range(stride*allocation_height))
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
depth[y*stride+x] = 0. if reverse else 1.
|
||||
for x, y in sources:
|
||||
depth[y*stride+x] = .9 if reverse else .1
|
||||
output = self.run_kernel('d4r_motion_dilate_lds', width, height, stride, allocation_height,
|
||||
depth, motion, radius, reverse)
|
||||
values = struct.unpack(f'<{len(output)//4}I', output)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
nearby = sorted(((x-sx)**2+(y-sy)**2, sx, sy) for sx, sy in sources)
|
||||
expected = motion[y*stride+x]
|
||||
if nearby[0][0] <= 64:
|
||||
_, sx, sy = nearby[0]
|
||||
expected = motion[sy*stride+sx]
|
||||
self.assertEqual(values[y*stride+x], expected, (x, y))
|
||||
|
||||
def test_randomized_bit_exact_generic_equivalence(self):
|
||||
import random
|
||||
rng = random.Random(0xD4A)
|
||||
for width, height in ((1, 1), (2, 3), (15, 17), (17, 31), (33, 19), (65, 37)):
|
||||
stride, allocation_height = ((width+7)//8+1)*8, height+3
|
||||
for scene in ('empty', 'sparse', 'dense', 'special'):
|
||||
forward = [float('nan')] * (stride*allocation_height)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
d = 1.
|
||||
if scene == 'dense' or (scene == 'sparse' and rng.random() < .12):
|
||||
d = rng.choice((.1, .5, .9, .99))
|
||||
elif scene == 'special':
|
||||
d = rng.choice((0., 1., 1.e-7, 1.-1.e-7, float('nan'), float('inf'), -float('inf'), .98, .99999994))
|
||||
forward[y*stride+x] = d
|
||||
motion = [rng.getrandbits(32) for _ in forward]
|
||||
for reverse in (False, True):
|
||||
depth = [1.-d for d in forward] if reverse else forward
|
||||
for radius in (1, 2):
|
||||
with self.subTest(size=(width, height), scene=scene, reverse=reverse, radius=radius):
|
||||
old = self.run_kernel('d4r_motion_dilate', width, height, stride, allocation_height,
|
||||
depth, motion, radius, reverse)
|
||||
new = self.run_kernel('d4r_motion_dilate_lds', width, height, stride, allocation_height,
|
||||
depth, motion, radius, reverse)
|
||||
self.assertEqual(old, new, 'LDS must have zero mismatched bytes')
|
||||
|
||||
def test_thin_sky_gate_matches_global_run_reference(self):
|
||||
# Wide roof plus a long pole, horizontal wire, diagonal and a short tip.
|
||||
# Moving the shapes across x/y=16 exercises the extra morphology halo.
|
||||
for shift in (0, 3):
|
||||
width, height, stride, ah = 47, 39, 56, 42
|
||||
foreground = {(x, y) for y in range(24, 39) for x in range(20+shift, 47)}
|
||||
foreground |= {(8+shift, y) for y in range(2, 29)}
|
||||
foreground |= {(x, 8+shift) for x in range(2, 38)}
|
||||
foreground |= {(x, x//2+12) for x in range(2, 20)}
|
||||
def short_run(x, y, dx, dy):
|
||||
length = 1
|
||||
for sign in (-1, 1):
|
||||
step = 1
|
||||
while (x+sign*step*dx, y+sign*step*dy) in foreground:
|
||||
length += 1
|
||||
step += 1
|
||||
return length <= 3
|
||||
eligible = [(x, y) for x, y in foreground
|
||||
if short_run(x,y,1,0) or short_run(x,y,0,1)]
|
||||
self.assertIn((8+shift, 18), eligible, 'long pole must survive')
|
||||
self.assertIn((18, 8+shift), eligible, 'horizontal wire must survive')
|
||||
self.assertNotIn((30+shift, 28), eligible, 'broad roof must be rejected')
|
||||
for reverse in (False, True):
|
||||
for radius in (1, 2):
|
||||
with self.subTest(shift=shift, reverse=reverse, radius=radius):
|
||||
depth = [float('nan')]*(stride*ah)
|
||||
motion = list(range(stride*ah))
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
depth[y*stride+x] = (.9 if reverse else .1) if (x,y) in foreground else (0. if reverse else 1.)
|
||||
out = self.run_kernel('d4r_motion_dilate_lds_thin', width, height, stride, ah,
|
||||
depth, motion, radius, reverse)
|
||||
values = struct.unpack(f'<{len(out)//4}I',out)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
bx, by = x, y
|
||||
# Opaque dilation is deliberately retained even where sky fill is gated.
|
||||
if (x,y) not in foreground:
|
||||
found = False
|
||||
for dy in range(-radius,radius+1):
|
||||
for dx in range(-radius,radius+1):
|
||||
nx, ny = max(0,min(width-1,x+dx)), max(0,min(height-1,y+dy))
|
||||
if not found and (nx,ny) in foreground:
|
||||
bx, by = nx, ny
|
||||
found = True
|
||||
near = sorted(((x-sx)**2+(y-sy)**2,sx,sy) for sx,sy in eligible)
|
||||
if near and near[0][0] <= 64:
|
||||
_, bx, by = near[0]
|
||||
self.assertEqual(values[y*stride+x], motion[by*stride+bx], (x,y))
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Dilation must not enter GPU input waits while lazy resources can synchronize."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class MotionSubmissionTests(unittest.TestCase):
|
||||
def test_gpu_wait_requires_a_warm_compatible_lds_slot(self):
|
||||
source = (ROOT / 'tools/d4r_nvngx_shim.cpp').read_text()
|
||||
begin = source.index('static bool motion_dilation_parameters(')
|
||||
end = source.index('static CudaObject prepare_dilated_motion(', begin)
|
||||
program = r'''
|
||||
#include "d4r_motion_dilation.h"
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
using CudaObject = uint64_t;
|
||||
static unsigned radius = 1;
|
||||
static unsigned env_uint(const char*, unsigned) { return radius; }
|
||||
struct HostPlane { unsigned width=20, height=20; size_t rowBytes=128; size_t size() const { return rowBytes*height; } };
|
||||
struct InputSlot {
|
||||
HostPlane host[3];
|
||||
struct { uint64_t device=1; } vram[3];
|
||||
uint64_t dilatedMotion=3, dilatedTexture=4;
|
||||
size_t dilatedBytes=2560, dilatedPitch=128;
|
||||
unsigned dilatedWidth=20, dilatedHeight=20;
|
||||
bool dilationEnqueued=false;
|
||||
};
|
||||
struct Feature { unsigned flags=2, outWidth=40, outHeight=40; bool linearInputs=true; };
|
||||
struct FrameParams {
|
||||
unsigned renderWidth=17, renderHeight=19;
|
||||
float jitterX=.25f, jitterY=-.25f;
|
||||
bool vram=true;
|
||||
unsigned colorBaseX=0, colorBaseY=0, depthBaseX=0, depthBaseY=0,
|
||||
mvBaseX=0, mvBaseY=0, outputBaseX=0, outputBaseY=0;
|
||||
};
|
||||
struct { struct { void* dilateMotion=(void*)1; void* dilateMotionPadded=(void*)1; } cu; } g;
|
||||
''' + source[begin:end] + r'''
|
||||
int main() {
|
||||
Feature f; InputSlot slot; FrameParams p;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,slot,p));
|
||||
slot.dilationEnqueued=true;
|
||||
assert(motion_dilation_gpu_wait_ready(f,slot,p));
|
||||
radius=2; assert(motion_dilation_gpu_wait_ready(f,slot,p));
|
||||
auto changed=slot; changed.host[2].rowBytes+=4;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,changed,p));
|
||||
changed=slot; ++changed.host[2].width;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,changed,p));
|
||||
changed=slot; ++changed.host[2].height;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,changed,p));
|
||||
changed=slot; changed.dilatedTexture=0;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,changed,p));
|
||||
changed=slot; changed.dilatedBytes=0;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,changed,p));
|
||||
g.cu.dilateMotionPadded=nullptr;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,slot,p));
|
||||
g.cu.dilateMotionPadded=(void*)1;
|
||||
p.depthBaseX=1; assert(!motion_dilation_gpu_wait_ready(f,slot,p));
|
||||
p.depthBaseX=0; f.flags=6; assert(!motion_dilation_gpu_wait_ready(f,slot,p));
|
||||
f.flags=0; f.outWidth=20; f.outHeight=20;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,slot,p)); // mapped display-res fallback
|
||||
f.flags=2; p.renderWidth=21;
|
||||
assert(!motion_dilation_gpu_wait_ready(f,slot,p)); // invalid allocation
|
||||
radius=0; assert(motion_dilation_gpu_wait_ready(f,slot,p)); // dilation off
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
runner = Path(temporary)/'submission'
|
||||
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror', '-I', str(ROOT/'tools'),
|
||||
'-x', 'c++', '-', '-o', str(runner)], input=program, text=True,
|
||||
check=True, capture_output=True)
|
||||
subprocess.run([str(runner)], check=True, timeout=5)
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Jitter-aligned native 3x3 motion without the extended halo."""
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS') == '1', 'opt-in GPU test')
|
||||
class NativeAlignedGpuTests(unittest.TestCase):
|
||||
def test_jitter_nearest_ties_partial_tiles_and_padding(self):
|
||||
runner = os.environ['D4R_DUMP_RUNNER']
|
||||
kernel = Path(os.environ.get('D4R_NATIVE_TEST_KERNEL', str(ROOT/'build/wine-nvcuda/generated'/
|
||||
(os.environ.get('D4R_GPU_ARCH', 'gfx1101')+'.hsaco'))))
|
||||
w, h, rw, rh = 33, 35, 11, 12
|
||||
dp, gp, op = w+3, w+5, w+7
|
||||
sentinel = 0xa5a5a5a5
|
||||
f32 = lambda x: struct.unpack('<f', struct.pack('<f', x))[0]
|
||||
depth = [f32(.1+((x*3+y*7)%5)*.1) for y in range(h) for x in range(w)]
|
||||
geometry = [struct.unpack('<I', struct.pack('<2e', .001*x, .001*y))[0] for y in range(h) for x in range(w)]
|
||||
for jx, jy in ((0., 0.), (.25, -.25), (-.5, .5), (1., -1.)):
|
||||
with self.subTest(jitter=(jx, jy)), tempfile.TemporaryDirectory() as tmp:
|
||||
d = Path(tmp); out = d/'out'; out.mkdir()
|
||||
padded_d = [float('nan')]*(dp*h)
|
||||
padded_g = [sentinel]*(gp*h)
|
||||
expected = [sentinel]*(op*h)
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
padded_d[y*dp+x] = depth[y*w+x]
|
||||
padded_g[y*gp+x] = geometry[y*w+x]
|
||||
cx = min(w-1, max(0, math.floor(f32(f32(x+.5)+f32(f32(jx*w)/rw)))))
|
||||
cy = min(h-1, max(0, math.floor(f32(f32(y+.5)+f32(f32(jy*h)/rh)))))
|
||||
best, bx, by = depth[cy*w+cx], cx, cy
|
||||
for dy in (-1, 0, 1):
|
||||
for dx in (-1, 0, 1):
|
||||
nx, ny = cx+dx, cy+dy
|
||||
if 0 <= nx < w and 0 <= ny < h and depth[ny*w+nx] < best:
|
||||
best, bx, by = depth[ny*w+nx], nx, ny
|
||||
expected[y*op+x] = geometry[by*w+bx]
|
||||
arrays = [struct.pack('<'+'f'*len(padded_d), *padded_d),
|
||||
struct.pack('<'+'I'*len(padded_g), *padded_g), bytes(1136), bytes(64),
|
||||
struct.pack('<'+'I'*(op*h), *([sentinel]*(op*h)))]
|
||||
addresses = [(i+1)*0x100000000 for i in range(5)]
|
||||
args = struct.pack('<8Q4I2f', *addresses, dp*4, gp*4, op*4, w, h, rw, rh, jx, jy)
|
||||
(d/'args.bin').write_bytes(args)
|
||||
manifest = ['kernel d4r_native_motion_compose_aligned', 'launch 3 3 1 256 1 1 0', 'args 88']
|
||||
for i, data in enumerate(arrays):
|
||||
(d/f'alloc-{i}.bin').write_bytes(data)
|
||||
manifest += [f'alloc {i} {hex(addresses[i])} {len(data)}', f'pointer {i*8} {i} 0']
|
||||
(d/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
subprocess.run([runner, str(kernel), 'd4r_native_motion_compose_aligned', str(d), str(out), '1'],
|
||||
check=True, capture_output=True)
|
||||
for i in range(4):
|
||||
self.assertEqual((out/f'alloc-{i}.bin').read_bytes(), arrays[i])
|
||||
self.assertEqual((out/'alloc-4.bin').read_bytes(), struct.pack('<'+'I'*len(expected), *expected))
|
||||
|
||||
def test_hybrid_preserves_game_vectors_away_from_silhouettes(self):
|
||||
runner = os.environ['D4R_DUMP_RUNNER']
|
||||
kernel = ROOT/'build/wine-nvcuda/generated'/(os.environ.get('D4R_GPU_ARCH', 'gfx1101')+'.hsaco')
|
||||
w,h,rw,rh=33,35,11,12
|
||||
pitch=(w+3)*4; gp=(rw+3)*4; sentinel=0xa5a5a5a5
|
||||
game=[struct.unpack('<I',struct.pack('<2e',.001*x,.001*y))[0] for y in range(rh) for x in range(rw)]
|
||||
padded_game=[sentinel]*((rw+3)*rh)
|
||||
for y in range(rh): padded_game[y*(rw+3):y*(rw+3)+rw]=game[y*rw:(y+1)*rw]
|
||||
for depth in (1.,.5):
|
||||
with self.subTest(depth=depth),tempfile.TemporaryDirectory() as tmp:
|
||||
d=Path(tmp);out=d/'out';out.mkdir()
|
||||
arrays=[struct.pack('<f',depth)*(w+3)*h,struct.pack('<I',0x3e003e00)*(w+3)*h,bytes(1136),bytes(64),
|
||||
struct.pack('<I',sentinel)*(w+3)*h,struct.pack('<'+'I'*len(padded_game),*padded_game)]
|
||||
addresses=[(i+1)*0x100000000 for i in range(6)]
|
||||
args=struct.pack('<8Q4I2f2Q',*addresses[:5],pitch,pitch,pitch,w,h,rw,rh,.25,-.25,addresses[5],gp)
|
||||
(d/'args.bin').write_bytes(args)
|
||||
manifest=['kernel d4r_native_motion_compose_hybrid','launch 3 3 1 256 1 1 0','args 104']
|
||||
for i,data in enumerate(arrays):
|
||||
(d/f'alloc-{i}.bin').write_bytes(data)
|
||||
manifest += [f'alloc {i} {hex(addresses[i])} {len(data)}',f'pointer {88 if i==5 else i*8} {i} 0']
|
||||
(d/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
subprocess.run([runner,str(kernel),'d4r_native_motion_compose_hybrid',str(d),str(out),'1'],check=True,capture_output=True)
|
||||
for i in (0,1,2,3,5): self.assertEqual((out/f'alloc-{i}.bin').read_bytes(),arrays[i])
|
||||
expected=[sentinel]*((w+3)*h)
|
||||
for y in range(h):
|
||||
for x in range(w): expected[y*(w+3)+x]=game[int((y+.5)*rh/h)*rw+int((x+.5)*rw/w)]
|
||||
self.assertEqual((out/'alloc-4.bin').read_bytes(),struct.pack('<'+'I'*len(expected),*expected))
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Native compositor ABI and rejection of unsafe buffer contracts."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class NativeMotionTests(unittest.TestCase):
|
||||
def test_buffer_ranges_and_pitches(self):
|
||||
source = r'''
|
||||
#include "d4r_native_motion.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
int main(void) {
|
||||
D4rNativeMotionParams p; memset(&p,0,sizeof(p));
|
||||
p.width=2560; p.height=1440; p.render_width=853; p.render_height=480;
|
||||
p.depth=0x10000000; p.geometry=0x20000000; p.scene=0x30000000;
|
||||
p.matrix=0x40000000; p.output=0x50000000;
|
||||
p.depth_pitch=10240; p.geometry_pitch=10244; p.output_pitch=10248;
|
||||
assert(sizeof(p)==80 && d4r_native_motion_parameters_valid(&p));
|
||||
assert(!d4r_native_motion_parameters_valid(0));
|
||||
D4rNativeMotionParams q=p;
|
||||
q.output=q.geometry+4; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.output=q.scene+16; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.output=q.matrix-16; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.depth=UINT64_MAX-16; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.output_pitch=UINT64_MAX-3; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.geometry_pitch=10239; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.depth_pitch=10241; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.render_width=2561; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.height=0; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.scene=0; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.matrix+=2; assert(!d4r_native_motion_parameters_valid(&q));
|
||||
q=p; q.height=1; q.render_height=1; q.output_pitch=10240; assert(d4r_native_motion_parameters_valid(&q));
|
||||
/* Adjacent, disjoint ranges are valid; output padding is not written. */
|
||||
q=p; q.output=q.geometry+(q.height-1)*q.geometry_pitch+q.width*4;
|
||||
assert(d4r_native_motion_parameters_valid(&q));
|
||||
D4rNativeMotionHybridParams hybrid={{p,0,0},0x60000000,853*4};
|
||||
assert(sizeof(hybrid)==104 && d4r_native_motion_hybrid_parameters_valid(&hybrid));
|
||||
D4rNativeMotionHybridParams bad=hybrid;bad.game_motion=bad.native.base.output;
|
||||
assert(!d4r_native_motion_hybrid_parameters_valid(&bad));
|
||||
bad=hybrid;bad.game_motion_pitch=853*4-1;assert(!d4r_native_motion_hybrid_parameters_valid(&bad));
|
||||
bad=hybrid;bad.native.jitter_x=2;assert(!d4r_native_motion_hybrid_parameters_valid(&bad));
|
||||
bad=hybrid;bad.game_motion=UINT64_MAX-16;assert(!d4r_native_motion_hybrid_parameters_valid(&bad));
|
||||
bad=hybrid;bad.game_motion_pitch=UINT64_MAX-3;assert(!d4r_native_motion_hybrid_parameters_valid(&bad));
|
||||
D4rNativeMotionPreparedParams prep={{p,0,0},0x70000000,0x80000000,1,0};
|
||||
assert(sizeof(prep)==112 && d4r_native_motion_prepared_parameters_valid(&prep));
|
||||
D4rNativeMotionPreparedParams no=prep;no.thin_mask=0;assert(!d4r_native_motion_prepared_parameters_valid(&no));
|
||||
no=prep;no.wire_axes=no.thin_mask;assert(!d4r_native_motion_prepared_parameters_valid(&no));
|
||||
no=prep;no.wire_axes=no.native.base.output;assert(!d4r_native_motion_prepared_parameters_valid(&no));
|
||||
no=prep;no.thin_mask=UINT64_MAX-3;assert(!d4r_native_motion_prepared_parameters_valid(&no));
|
||||
no=prep;no.wire_flags=4;assert(!d4r_native_motion_prepared_parameters_valid(&no));
|
||||
no=prep;no.reserved=1;assert(!d4r_native_motion_prepared_parameters_valid(&no));
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
runner = Path(tmp) / "native-motion"
|
||||
for compiler, language, standard in [("cc", "c", "c11"), ("c++", "c++", "c++17")]:
|
||||
subprocess.run([compiler, "-x", language, "-std=" + standard, "-Wall", "-Wextra", "-Werror",
|
||||
"-I", str(ROOT / "tools"), "-o", str(runner), "-"],
|
||||
input=source, text=True, check=True, capture_output=True)
|
||||
subprocess.run([str(runner)], check=True, timeout=5)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Execute the shim's motion routing with native readiness and NGX failures."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class NativeMotionRoutingTests(unittest.TestCase):
|
||||
def test_independent_repair_and_ngx_motion(self):
|
||||
source = (ROOT / 'tools/d4r_nvngx_shim.cpp').read_text()
|
||||
begin = source.index(' CudaObject nativeMotion = 0;')
|
||||
end = source.index(' // Ray Reconstruction: the auxiliary', begin)
|
||||
block = source[begin:end]
|
||||
begin = source.index(' const bool wireHistoryRequested=nativeMotion')
|
||||
end = source.index(';', begin) + 1
|
||||
wire = source[begin:end]
|
||||
program = r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
using CudaObject = uint64_t;
|
||||
static unsigned substitute=1, repair=1;
|
||||
static bool available=true, matched=true;
|
||||
static CudaObject selected=0;
|
||||
static unsigned env_uint(const char* name, unsigned) {
|
||||
return std::strcmp(name,"D4R_NATIVE_MV_SUBSTITUTE")==0 ? substitute : repair;
|
||||
}
|
||||
static void logf(const char*, ...) {}
|
||||
struct Feature {
|
||||
unsigned flags=0x43, cudaFlags=0x43, outWidth=2560, outHeight=1440;
|
||||
void* cudaHandle=(void*)1;
|
||||
};
|
||||
struct Slot { bool nativeLive=true; };
|
||||
struct Params { unsigned renderWidth=853,renderHeight=480; float mvScaleX=853,mvScaleY=480; };
|
||||
static CudaObject compose_native_live(Feature&, Slot&, const Params&, unsigned, void*) { return available ? 42 : 0; }
|
||||
static bool set_native_motion_flags(Feature& f, Params&, bool native, unsigned) {
|
||||
if(matched) f.cudaFlags=native ? f.flags & ~2u : f.flags;
|
||||
return matched;
|
||||
}
|
||||
static CudaObject prepare_dilated_motion(Feature&, Slot&, const Params&) { return 7; }
|
||||
struct { struct { bool stabilizeThinWires=true; } cu; } g;
|
||||
static void run(bool nativeLive, bool hasField, unsigned useNative, unsigned useRepair,
|
||||
bool match, unsigned initialFlags, bool expectedOk,
|
||||
CudaObject expectedMotion, bool expectedRepair) {
|
||||
Feature state; auto* feature=&state; Slot slot; Params params;
|
||||
slot.nativeLive=nativeLive; available=hasField; substitute=useNative;repair=useRepair;matched=match;
|
||||
state.cudaFlags=initialFlags;
|
||||
bool ok=true; unsigned frame=1; void* stages=nullptr;
|
||||
''' + block + wire + r'''
|
||||
selected=liveMotion ? liveMotion : dilatedMotion;
|
||||
assert(ok==expectedOk);
|
||||
assert(selected==expectedMotion);
|
||||
assert(wireHistoryRequested==expectedRepair);
|
||||
if(liveMotion) {
|
||||
assert(params.mvScaleX==2560 && params.mvScaleY==1440);
|
||||
assert(state.cudaFlags==0x41);
|
||||
} else {
|
||||
assert(params.mvScaleX==853 && params.mvScaleY==480);
|
||||
if(ok) assert(state.cudaFlags==0x43);
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
run(true,true,1,1,true,0x43,true,42,true); // old combined path
|
||||
run(true,true,1,0,true,0x43,true,42,false); // native only
|
||||
run(true,true,0,1,true,0x43,true,7,true); // game MVs + repair
|
||||
run(true,true,0,0,true,0x43,true,7,false); // game MVs, no repair
|
||||
run(false,false,1,1,true,0x43,true,7,false); // clean baseline
|
||||
run(true,false,0,1,true,0x43,true,7,false); // unavailable native inputs
|
||||
run(true,true,0,1,true,0x41,true,7,true); // restore low-res NGX flags
|
||||
run(true,true,0,1,false,0x41,false,0,true); // failed restore must not evaluate
|
||||
run(true,true,1,1,false,0x43,true,7,false); // preserve old raw-output fallback
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
runner = Path(temp) / 'routing'
|
||||
subprocess.run(['c++', '-std=c++17', '-Wall', '-Wextra', '-Werror', '-x', 'c++', '-',
|
||||
'-o', str(runner)], input=program, text=True, check=True, capture_output=True)
|
||||
subprocess.run([str(runner)], check=True, timeout=5)
|
||||
@@ -91,7 +91,7 @@ class OptiScalerConfigTests(unittest.TestCase):
|
||||
inputs.mkdir()
|
||||
(repo / 'scripts').mkdir(parents=True)
|
||||
for name in ('package_release.sh', 'configure_optiscaler.py', 'check_glibc_compat.sh',
|
||||
'check_redistributable.py', 'private_d3d12_core.py'):
|
||||
'check_redistributable.py', 'private_d3d12_core.py', 'stage_engine_kit.sh'):
|
||||
shutil.copy2(ROOT / 'scripts' / name, repo / 'scripts' / name)
|
||||
shutil.copytree(ROOT / 'packaging', repo / 'packaging')
|
||||
for name in ('LICENSE', 'NOTICE'):
|
||||
@@ -113,6 +113,18 @@ class OptiScalerConfigTests(unittest.TestCase):
|
||||
path.write_bytes(data)
|
||||
return path
|
||||
|
||||
# Exercise the actual installer-kit stager with representative source files.
|
||||
# No model builder, shader compiler or GPU fixture is executed here.
|
||||
for name in ('engine/compile_k.py', 'engine/test_k.py', 'engine/test_m.py',
|
||||
'engine/rr/test_layer.py', 'vulkan/ptx2glsl.py'):
|
||||
destination = repo / name
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / name, destination)
|
||||
fixture(repo / 'engine/weights.bin', b'must not enter the model kit')
|
||||
fixture(repo / 'engine/test_unrelated.py', b'must not enter the model kit')
|
||||
fixture(repo / 'build/d3d12_dlss_harness.exe')
|
||||
validator = fixture(inputs / 'glslang/bin/glslangValidator', b'packaging-only compiler fixture')
|
||||
|
||||
# A real minimal ELF lets the package's existing GLIBC checker run.
|
||||
elf = inputs / 'library.so'
|
||||
subprocess.run(['cc', '-shared', '-x', 'c', '-o', str(elf), '-'],
|
||||
@@ -153,7 +165,8 @@ class OptiScalerConfigTests(unittest.TestCase):
|
||||
D4R_ZLUDA_DIR=str(inputs / 'zluda'), D4R_VKD3D_DIR=str(inputs / 'vkd3d'),
|
||||
D4R_ROCM_RUNTIME=str(inputs / 'rocm'), D4R_OPTISCALER_LICENSE=str(license_path),
|
||||
D4R_ZLUDA_SRC=str(inputs / 'source'), D4R_VKD3D_SRC=str(inputs / 'source'),
|
||||
D4R_BUNDLE_DLSS=str(dlss), D4R_BUNDLE_NGX=str(core), D4R_BUNDLE_TEX=str(inputs / 'tex'))
|
||||
D4R_BUNDLE_DLSS=str(dlss), D4R_BUNDLE_NGX=str(core), D4R_BUNDLE_TEX=str(inputs / 'tex'),
|
||||
D4R_GLSLANG=str(inputs / 'glslang'), D4R_GLSLANG_LICENSE=str(license_path))
|
||||
version = (repo / 'packaging/VERSION').read_text().strip()
|
||||
configs = []
|
||||
# A compiled engine model: only d4r's shaders and marker may reach a zip.
|
||||
@@ -186,12 +199,32 @@ class OptiScalerConfigTests(unittest.TestCase):
|
||||
self.assertEqual(archive.read('d4r.asi'), archive.read('d4r/nvngx.dll'))
|
||||
self.assertIn('d4r/d4r-setup.exe', archive.namelist())
|
||||
self.assertEqual('d4r/install.sh' in archive.namelist(), bundled == 'installer')
|
||||
self.assertFalse(any(n.endswith('.py') for n in archive.namelist()))
|
||||
if bundled != 'installer':
|
||||
self.assertFalse(any(n.endswith('.py') for n in archive.namelist()))
|
||||
self.assertIn(b'ASI add-on', archive.read('d4r/README.txt'))
|
||||
self.assertEqual({n.split('/')[0] for n in archive.namelist()}, {'d4r.asi', 'd4r'})
|
||||
self.assertIn('d4r/d3d12.dll', archive.namelist())
|
||||
self.assertIn('d4r/d4r12core.dll', archive.namelist())
|
||||
self.assertNotIn('d4r/d3d12core.dll', archive.namelist())
|
||||
kit = 'd4r/tools/models/'
|
||||
self.assertEqual('d4r/tools/build-engine-models.sh' in archive.namelist(), bundled == 'installer')
|
||||
if bundled == 'installer':
|
||||
self.assertEqual(archive.read('d4r/tools/build-engine-models.sh'),
|
||||
(ROOT / 'packaging/build_engine_models.sh').read_bytes())
|
||||
for executable in ('d4r/tools/build-engine-models.sh', kit + 'bin/glslangValidator'):
|
||||
self.assertTrue((archive.getinfo(executable).external_attr >> 16) & 0o111)
|
||||
for source in ('engine/compile_k.py', 'engine/test_k.py', 'engine/test_m.py',
|
||||
'engine/rr/test_layer.py', 'vulkan/ptx2glsl.py'):
|
||||
self.assertEqual(archive.read(kit + source), (ROOT / source).read_bytes())
|
||||
self.assertEqual(archive.read(kit + 'bin/glslangValidator'), validator.read_bytes())
|
||||
self.assertEqual(archive.read('d4r/licenses/glslang-LICENSE.txt'), license_path.read_bytes())
|
||||
self.assertIn(kit + 'bin/d3d12_dlss_harness.exe', archive.namelist())
|
||||
self.assertFalse(any(n.endswith('.py') and not n.startswith(kit)
|
||||
for n in archive.namelist()))
|
||||
self.assertNotIn(kit + 'engine/weights.bin', archive.namelist())
|
||||
self.assertNotIn(kit + 'engine/test_unrelated.py', archive.namelist())
|
||||
else:
|
||||
self.assertFalse(any(n.startswith(kit) for n in archive.namelist()))
|
||||
self.assertIn('d4r/nvngx.dll', archive.namelist())
|
||||
self.assertNotIn('nvngx.dll', archive.namelist())
|
||||
self.assertNotIn('d4r/_nvngx.dll', archive.namelist())
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""R11G11B10 storage conversion must match the original blit for all half inputs."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS')=='1','opt-in GPU test')
|
||||
class OutputConversionGpuTests(unittest.TestCase):
|
||||
def test_fused_r11_matches_blit(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
runner=Path(temp)/'conversion'
|
||||
subprocess.run(['c++','-std=c++17','-O2','-Wall','-Wextra','-Werror',
|
||||
'-Wno-missing-field-initializers','-I'+str(ROOT/'tools'),
|
||||
'-I'+str(ROOT/'build/vulkan-staging'),
|
||||
str(ROOT/'tools/vulkan_output_conversion_probe.cpp'),'-lvulkan','-o',str(runner)],
|
||||
check=True,capture_output=True,text=True)
|
||||
subprocess.run([str(runner)],check=True,capture_output=True,text=True,timeout=30)
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Prepared geometry keeps rendered output and readable temporal state exact."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import random
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS') == '1', 'opt-in GPU test')
|
||||
class PreparedWireGpuTests(unittest.TestCase):
|
||||
def test_prepared_matches_reference_with_poisoned_invalid_history(self):
|
||||
runner = os.environ['D4R_DUMP_RUNNER']
|
||||
kernel = ROOT/'build/wine-nvcuda/generated'/(os.environ.get('D4R_GPU_ARCH', 'gfx1101')+'.hsaco')
|
||||
w,h,rw,rh=65,67,21,22
|
||||
pitch,dp,mp,cp,idp=w*8+16,w*4+12,w*4+20,rw*8+16,rw*4+12
|
||||
pixels=w*h
|
||||
rng=random.Random(746)
|
||||
rgba=lambda: struct.pack('<4e',rng.uniform(.05,1),rng.uniform(.05,1),rng.uniform(.05,1),.5)
|
||||
current=b''.join(b''.join(rgba() for _ in range(w))+b'\xa5'*16 for _ in range(h))
|
||||
source=b''.join(struct.pack('<4e',.125,.125,.125,.75)*rw+b'\xa5'*16 for _ in range(rh))
|
||||
previous=bytearray(b''.join(struct.pack('<4e',.125,.125,.125,.75)*w+b'\xa5'*16 for _ in range(h)))
|
||||
ages=bytes(rng.choice((0,3,4,15,16,255)) for _ in range(pixels))
|
||||
previous_depth=bytearray(struct.pack('<f',.5)*pixels)
|
||||
for i,age in enumerate(ages):
|
||||
if age==255:
|
||||
y,x=divmod(i,w)
|
||||
previous[y*pitch+x*8:y*pitch+x*8+8]=struct.pack('<4H',0x7e00,0x7e00,0x7e00,0x7e00)
|
||||
previous_depth[i*4:i*4+4]=struct.pack('<f',float('nan'))
|
||||
for flags in (0,1,2,3):
|
||||
for jitter in ((0.,0.),(.25,-.25),(-1.,1.)):
|
||||
for pattern in ('sky','flat','wire'):
|
||||
with self.subTest(flags=flags,jitter=jitter,pattern=pattern), tempfile.TemporaryDirectory() as tmp:
|
||||
d=Path(tmp);out=d/'out';out.mkdir()
|
||||
z=[1. if pattern=='sky' else .5 if pattern=='flat' or x==32 else 1.
|
||||
for y in range(h) for x in range(w)]
|
||||
if pattern=='wire':z[10*w+31]=float('nan')
|
||||
depth=b''.join(struct.pack('<'+'f'*w,*z[y*w:(y+1)*w])+b'\xa5'*12 for y in range(h))
|
||||
arrays=[current,source,depth,b''.join(bytes(w*4)+b'\xa5'*20 for _ in range(h)),
|
||||
bytes(previous),ages,bytes(previous_depth),b'\xa5'*(pitch*h),
|
||||
b'\xa5'*(pixels+64),b'\xa5'*(pixels*4+64),
|
||||
b''.join(struct.pack('<f',.5)*rw+b'\xa5'*12 for _ in range(rh)),
|
||||
b'\xa5'*(pitch*h),b'\xa5'*(pixels+64),
|
||||
b'\xa5'*(((w+31)//32)*h*4+64),b'\xa5'*(pixels+64),
|
||||
b'\xa5'*(mp*h),bytes(1136),bytes(64)]
|
||||
addresses=[(i+1)*0x100000000 for i in range(len(arrays))]
|
||||
def reset():
|
||||
for i,data in enumerate(arrays):(d/f'alloc-{i}.bin').write_bytes(data)
|
||||
def launch(name,args,pointers,grid):
|
||||
(d/'args.bin').write_bytes(args)
|
||||
manifest=['kernel '+name,'launch '+' '.join(map(str,(*grid,1,256,1,1,0))),f'args {len(args)}']
|
||||
for i,data in enumerate(arrays):manifest.append(f'alloc {i} {hex(addresses[i])} {len(data)}')
|
||||
manifest += [f'pointer {offset} {allocation} 0' for offset,allocation in pointers]
|
||||
(d/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
subprocess.run([runner,str(kernel),name,str(d),str(out),'1'],check=True,capture_output=True)
|
||||
for i in range(len(arrays)):(d/f'alloc-{i}.bin').write_bytes((out/f'alloc-{i}.bin').read_bytes())
|
||||
args=struct.pack('<17Q5I2fI',*addresses[:12],cp,dp,mp,pitch,idp,w,h,rw,rh,1,*jitter,flags)
|
||||
base_pointers=[(i*8,i) for i in range(12)]
|
||||
reset();launch('d4r_wire_history',args,base_pointers,((w+15)//16,(h+15)//16))
|
||||
expected={i:(out/f'alloc-{i}.bin').read_bytes() for i in (7,8,9,11)}
|
||||
reset()
|
||||
native=struct.pack('<8Q4I2f2Q2I',addresses[2],addresses[3],addresses[16],addresses[17],addresses[15],
|
||||
dp,mp,mp,w,h,rw,rh,*jitter,addresses[13],addresses[14],flags,0)
|
||||
native_pointers=[(0,2),(8,3),(16,16),(24,17),(32,15),(88,13),(96,14)]
|
||||
launch('d4r_native_motion_prepare',native,native_pointers,((w+31)//32,(h+31)//32))
|
||||
prepared=args+struct.pack('<2Q',addresses[12],addresses[14])
|
||||
launch('d4r_wire_history_preclassified',prepared,base_pointers+[(168,12),(176,14)],((w+15)//16,(h+15)//16))
|
||||
launch('d4r_wire_history_apply',prepared[:176],base_pointers+[(168,12)],((w+15)//16,(h+15)//16))
|
||||
actual={i:(out/f'alloc-{i}.bin').read_bytes() for i in range(len(arrays))}
|
||||
self.assertEqual(actual[0],expected[7])
|
||||
self.assertEqual(actual[8],expected[8])
|
||||
for i in (1,2,3,4,5,6,10,16,17):self.assertEqual(actual[i],arrays[i])
|
||||
for i in range(pixels):
|
||||
if expected[8][i]!=255:
|
||||
y,x=divmod(i,w);offset=y*pitch+x*8
|
||||
self.assertEqual(actual[11][offset:offset+8],expected[11][offset:offset+8])
|
||||
self.assertEqual(actual[9][i*4:i*4+4],expected[9][i*4:i*4+4])
|
||||
for y in range(h):self.assertEqual(actual[11][y*pitch+w*8:(y+1)*pitch],b'\xa5'*16)
|
||||
for i,start in ((8,pixels),(9,pixels*4),(12,pixels),(13,((w+31)//32)*h*4),(14,pixels)):
|
||||
self.assertEqual(actual[i][start:],b'\xa5'*64)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Keep startup, feature instances and resolution changes out of shared medians."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location('profile_summary', ROOT/'scripts/summarize_d4r_profile.py')
|
||||
summary = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(summary)
|
||||
|
||||
|
||||
class ProfileSummaryTests(unittest.TestCase):
|
||||
def test_warm_groups_and_unavailable_timings(self):
|
||||
def sample(frame, render='853x480', reset=0, gpu='2.2'):
|
||||
return (f'D4R_PROFILE frame={frame} render={render} output=2560x1440 '
|
||||
f'preset=11 quality=3 flags=0x43 input_wait=cpu reset={reset} '
|
||||
f'vk_total=2.8 dlss_gpu={gpu} native_motion_gpu=-1 wire_repair_gpu=-1')
|
||||
create='NVSDK_NGX_D3D12_CreateFeature(feature=1)'
|
||||
log='\n'.join((create, sample(1), sample(300, reset=1), sample(600),
|
||||
sample(900, gpu='nan'), sample(1200, render='1706x960'),
|
||||
create, sample(300)))
|
||||
rows=summary.summarize(log)
|
||||
self.assertEqual(len(rows), 3)
|
||||
self.assertEqual([r['samples'] for r in rows], [2, 1, 1])
|
||||
self.assertEqual([r['feature_instance'] for r in rows], [1, 1, 2])
|
||||
self.assertEqual(rows[0]['timings_ms']['dlss_gpu']['n'], 1)
|
||||
self.assertIsNone(rows[0]['timings_ms']['wire_repair_gpu'])
|
||||
self.assertEqual(rows[0]['timings_ms']['vk_total']['median'], 2.8)
|
||||
|
||||
def test_routing_changes_get_separate_groups(self):
|
||||
base = ('D4R_PROFILE frame=300 render=853x480 output=2560x1440 '
|
||||
'preset=11 quality=3 flags=0x43 input_wait=cpu reset=0 '
|
||||
'feature=29184 native_field=1 wire_requested=1 gpu_completion=0 ')
|
||||
rows = summary.summarize(base+'native_substitute=0\n'+base+'native_substitute=1')
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual([r['native_substitute'] for r in rows], ['0', '1'])
|
||||
|
||||
def test_scene_window_and_actual_output_mode(self):
|
||||
base = ('D4R_PROFILE frame=600 render=853x480 output=2560x1440 '
|
||||
'preset=11 quality=3 flags=0x43 input_wait=cpu reset=0 ')
|
||||
log = ('[ 10.000] '+base+'vk_total=8 vk_output_mode=transfer\n'
|
||||
'[ 20.000] '+base+'vk_total=3 vk_output_mode=compute\n'
|
||||
'[ 21.000] '+base+'vk_total=4 vk_output_mode=transfer\n'
|
||||
'[ 30.000] '+base+'vk_total=9 vk_output_mode=compute')
|
||||
rows = summary.summarize(log, since_clock=19, until_clock=22)
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual([r['vk_output_mode'] for r in rows], ['compute', 'transfer'])
|
||||
self.assertEqual([r['timings_ms']['vk_total']['median'] for r in rows], [3, 4])
|
||||
@@ -7,7 +7,8 @@ import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CONFIG = ROOT / "scripts" / "d4r_config.py"
|
||||
RR_VARIABLES = ("D4R_RR_ENABLE", "D4R_RR_PRESET")
|
||||
RR_VARIABLES = ("D4R_RR_ENABLE", "D4R_RR_PRESET", "D4R_RR_BACKEND", "D4R_FSR_RR_DLL", "D4R_FSR_RR_STABILITY_BIAS",
|
||||
"D4R_FSR_RR_DETAIL")
|
||||
|
||||
|
||||
def run_config(text, **overrides):
|
||||
@@ -68,5 +69,64 @@ class RayReconstructionConfigTests(unittest.TestCase):
|
||||
self.assertNotIn("D4R_RR_PRESET", decided)
|
||||
|
||||
|
||||
class RayReconstructionBackendTests(unittest.TestCase):
|
||||
"""[RayReconstruction] Backend picks the denoiser; it never changes the DLSS presets."""
|
||||
|
||||
def test_default_leaves_dlss_in_place(self):
|
||||
code, decided = run_config("[RayReconstruction]\nEnable = true\n")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertNotIn("D4R_RR_BACKEND", decided) # the shim's default is dlss
|
||||
|
||||
def test_backends_are_exported_and_independent_of_presets(self):
|
||||
for backend in ("dlss", "fsr", "FSR"):
|
||||
with self.subTest(backend=backend):
|
||||
code, decided = run_config(f"[RayReconstruction]\nBackend = {backend}\nModel = E\n[DLSS]\nModel = K\n")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(decided["D4R_RR_BACKEND"], backend.lower())
|
||||
self.assertEqual(decided["D4R_RR_PRESET"], "5")
|
||||
self.assertEqual(decided["D4R_DLSS_PRESET"], "11")
|
||||
|
||||
def test_unknown_backend_is_rejected(self):
|
||||
for value in ("amd", "fsr4", "native"):
|
||||
with self.subTest(value=value):
|
||||
code, decided = run_config(f"[RayReconstruction]\nBackend = {value}\n")
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertNotIn("D4R_RR_BACKEND", decided)
|
||||
|
||||
def test_denoiser_path_and_environment_precedence(self):
|
||||
code, decided = run_config("[RayReconstruction]\nBackend = fsr\nFsrDenoiser = /opt/amd/denoiser.dll\n")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(decided["D4R_FSR_RR_DLL"], "/opt/amd/denoiser.dll")
|
||||
code, decided = run_config("[RayReconstruction]\nBackend = fsr\n", D4R_RR_BACKEND="dlss")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertNotIn("D4R_RR_BACKEND", decided)
|
||||
|
||||
def test_templates_keep_dlss_as_the_default(self):
|
||||
import configparser
|
||||
for name in ("config/d4r.ini.default", "packaging/d4r.ini"):
|
||||
ini = configparser.ConfigParser(inline_comment_prefixes=(";",))
|
||||
ini.read(ROOT / name)
|
||||
self.assertEqual(ini.get("RayReconstruction", "Backend"), "dlss", name)
|
||||
self.assertEqual(ini.get("RayReconstruction", "FsrDenoiser"), "auto", name)
|
||||
self.assertEqual(ini.get("RayReconstruction", "FsrStabilityBias"), "auto", name)
|
||||
self.assertEqual(ini.get("RayReconstruction", "FsrDetail"), "0", name)
|
||||
|
||||
def test_fsr_tuning_keys(self):
|
||||
code, decided = run_config("[RayReconstruction]\nBackend = fsr\nFsrStabilityBias = auto\nFsrDetail = 0.25\n")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertNotIn("D4R_FSR_RR_STABILITY_BIAS", decided) # auto: AMD's own default
|
||||
self.assertEqual(decided["D4R_FSR_RR_DETAIL"], "0.25")
|
||||
code, decided = run_config("[RayReconstruction]\nFsrStabilityBias = 0\n")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(decided["D4R_FSR_RR_STABILITY_BIAS"], "0")
|
||||
for text in ("FsrStabilityBias = -1", "FsrStabilityBias = low", "FsrDetail = 1.5", "FsrDetail = x"):
|
||||
with self.subTest(text=text):
|
||||
code, _ = run_config(f"[RayReconstruction]\n{text}\n")
|
||||
self.assertNotEqual(code, 0)
|
||||
code, decided = run_config("[RayReconstruction]\nFsrStabilityBias = 0\n", D4R_FSR_RR_STABILITY_BIAS="1")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertNotIn("D4R_FSR_RR_STABILITY_BIAS", decided)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""CPU camera transforms: inversion and relative camera motion before RR dispatch."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class RrCameraTests(unittest.TestCase):
|
||||
def test_inverse_and_relative_camera_translation(self):
|
||||
source = r'''
|
||||
#include "engine/rr_camera.h"
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
using namespace d4r::rr_camera;
|
||||
int main() {
|
||||
const Matrix identity{1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1};
|
||||
// A dense, asymmetric matrix catches row/column and cofactor-sign mistakes.
|
||||
const Matrix dense{2,1,3,4, 0,3,2,1, 1,0,4,2, 2,3,0,5};
|
||||
const Matrix product = multiply(dense, inverse(dense));
|
||||
for (int i = 0; i < 16; ++i) assert(std::abs(product[i] - identity[i]) < 2e-6f);
|
||||
Matrix current = identity, previous = identity;
|
||||
current[12] = 3; previous[12] = 2;
|
||||
const auto camera = transforms(current, identity, previous, identity, 4, 2);
|
||||
Matrix expectedPrevious = identity; expectedPrevious[12] = -1;
|
||||
const Matrix pixels{2,0,0,0, 0,-1,0,0, 0,0,1,0, 2,1,0,1};
|
||||
const Matrix expectedPixels = multiply(expectedPrevious, pixels);
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
assert(camera.previousClip[i] == expectedPrevious[i]);
|
||||
assert(camera.inverseClip[i] == identity[i]);
|
||||
assert(camera.previousPixels[i] == expectedPixels[i]);
|
||||
}
|
||||
const Matrix inversePixelProduct = multiply(pixels, camera.inversePixels);
|
||||
for (int i = 0; i < 16; ++i) assert(inversePixelProduct[i] == identity[i]);
|
||||
// Reset is represented by passing the current camera as the previous camera.
|
||||
const auto reset = transforms(current, identity, current, identity, 4, 2);
|
||||
for (int i = 0; i < 16; ++i) assert(reset.previousClip[i] == identity[i]);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
runner = Path(temporary) / 'camera'
|
||||
subprocess.run(['c++', '-O2', '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-I', str(ROOT), '-x', 'c++', '-', '-o', str(runner)],
|
||||
input=source, text=True, check=True, capture_output=True)
|
||||
subprocess.run([str(runner)], check=True, timeout=10)
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Engine opt-in, preset-selection behavior and polling configuration precedence."""
|
||||
"""Engine defaults, preset-selection behavior and polling configuration precedence."""
|
||||
import configparser
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -10,17 +10,47 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ShimConfigTests(unittest.TestCase):
|
||||
def test_engine_is_opt_in_and_configured_by_ini(self):
|
||||
def test_profile_log_interval_validation_and_environment_precedence(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
ini = Path(temporary) / 'd4r.ini'
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith('D4R_')}
|
||||
cmd = ['python3', str(ROOT / 'scripts/d4r_config.py'), '--config', str(ini)]
|
||||
for value, expected in ((None, None), ('auto', None), ('1', '1'), ('60', '60'),
|
||||
('060', '60'), ('120', '120'), ('4294967295', '4294967295')):
|
||||
ini.write_text('[Debug]\nProfile = true\n' +
|
||||
(f'ProfileLogEvery = {value}\n' if value is not None else ''))
|
||||
result = subprocess.run(cmd, env=env, text=True, capture_output=True, check=True)
|
||||
self.assertIn('export D4R_PROFILE=1\n', result.stdout)
|
||||
if expected is None:
|
||||
self.assertNotIn('export D4R_PROFILE_LOG_EVERY=', result.stdout)
|
||||
else:
|
||||
self.assertIn(f'export D4R_PROFILE_LOG_EVERY={expected}\n', result.stdout)
|
||||
ini.write_text('[Debug]\nProfileLogEvery = 60\n')
|
||||
result = subprocess.run(cmd, env=dict(env, D4R_PROFILE_LOG_EVERY='120'),
|
||||
text=True, capture_output=True, check=True)
|
||||
self.assertNotIn('export D4R_PROFILE_LOG_EVERY=', result.stdout)
|
||||
for value in ('0', '-1', '0.5', 'invalid', '4294967296'):
|
||||
ini.write_text(f'[Debug]\nProfileLogEvery = {value}\n')
|
||||
result = subprocess.run(cmd, env=env, text=True, capture_output=True)
|
||||
self.assertEqual(result.returncode, 2, value)
|
||||
for name in ('packaging/d4r.ini', 'config/d4r.ini.default'):
|
||||
ini = configparser.ConfigParser(inline_comment_prefixes=(';',))
|
||||
ini.read(ROOT / name)
|
||||
self.assertFalse(ini.getboolean('Engine', 'Enabled'), name)
|
||||
self.assertEqual(ini.getint('Debug', 'ProfileLogEvery'), 60, name)
|
||||
|
||||
def test_engine_defaults_and_ini_overrides(self):
|
||||
for name, expected in (('packaging/d4r.ini', 'auto'), ('config/d4r.ini.default', 'false')):
|
||||
ini = configparser.ConfigParser(inline_comment_prefixes=(';',))
|
||||
ini.read(ROOT / name)
|
||||
self.assertEqual(ini.get('Engine', 'Enabled'), expected, name)
|
||||
self.assertEqual(ini.get('Engine', 'ModelDir'), 'auto', name)
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
ini = Path(temporary) / 'd4r.ini'
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith('D4R_')}
|
||||
cmd = ['python3', str(ROOT / 'scripts/d4r_config.py'), '--config', str(ini)]
|
||||
for text, enabled, model in (('', '0', None), ('[Engine]\nEnabled = true\n', '1', None),
|
||||
for text, enabled, model in (('', '0', None), ('[Engine]\nEnabled = auto\n', '0', None),
|
||||
('[Engine]\nEnabled = false\n', '0', None),
|
||||
('[Engine]\nEnabled = true\n', '1', None),
|
||||
('[Engine]\nEnabled = true\nModelDir = /models/k\n', '1', '/models/k')):
|
||||
ini.write_text(text)
|
||||
result = subprocess.run(cmd, env=env, text=True, capture_output=True, check=True)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Conversion labels must belong to the older Vulkan query slot, not this frame."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class VramProfileTimingTests(unittest.TestCase):
|
||||
def test_older_slot_mode_labels(self):
|
||||
source=(ROOT/'tools/d4r_nvngx_shim.cpp').read_text()
|
||||
start=source.index('static void read_profile_timestamps(Feature& feature, uint32_t frame, FrameTiming& timing)\n{')
|
||||
end=source.index('\n}\n',start)+3
|
||||
program=r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
constexpr unsigned kOutputSlots=4;
|
||||
struct FrameTiming {double vkInput=-1,vkDlssWait=-1,vkOutput=-1,vkTotal=-1;int vkOutputFused=-1,vkInputFused=-1;};
|
||||
struct Feature {uint64_t values[16]={};volatile uint64_t* timestampValues=values;double timestampTicksPerMs=10;
|
||||
uint8_t timestampOutputFused[4]={},timestampInputFused[4]={};bool recordOutputFused=true,recordInputFused=true;};
|
||||
''' + source[start:end] + r'''
|
||||
int main() {
|
||||
Feature f;FrameTiming old;
|
||||
f.values[0]=10;f.values[1]=20;f.values[2]=50;f.values[3]=60;
|
||||
f.timestampOutputFused[0]=0;f.timestampInputFused[0]=1;
|
||||
read_profile_timestamps(f,300,old); // frame 296 used transfer output, despite current mode switching on
|
||||
assert(old.vkOutputFused==0 && old.vkInputFused==1 && old.vkTotal==5);
|
||||
assert(!f.recordOutputFused && !f.recordInputFused && f.values[0]==0);
|
||||
f.values[0]=70;f.values[1]=80;f.values[2]=110;f.values[3]=120;
|
||||
f.timestampOutputFused[0]=1;f.timestampInputFused[0]=0;FrameTiming next;
|
||||
read_profile_timestamps(f,304,next);
|
||||
assert(next.vkOutputFused==1 && next.vkInputFused==0);
|
||||
FrameTiming unavailable;read_profile_timestamps(f,308,unavailable);
|
||||
assert(unavailable.vkOutputFused==-1 && unavailable.vkInputFused==-1 && unavailable.vkTotal==-1);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
runner=Path(temp)/'timing'
|
||||
subprocess.run(['c++','-std=c++17','-Wall','-Wextra','-Werror','-x','c++','-',
|
||||
'-o',str(runner)],input=program,text=True,check=True,capture_output=True)
|
||||
subprocess.run([str(runner)],check=True,timeout=5)
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Fractional wire coverage must preserve a different foreground surface."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS')=='1','opt-in GPU test')
|
||||
class WireBackgroundGpuTests(unittest.TestCase):
|
||||
def test_sky_wire_does_not_spill_onto_other_geometry(self):
|
||||
runner=os.environ['D4R_DUMP_RUNNER']
|
||||
kernel=Path(os.environ.get('D4R_WIRE_TEST_KERNEL',str(ROOT/'build/wine-nvcuda/generated'/'gfx1101.hsaco')))
|
||||
w,h=65,67
|
||||
pitch,dp,mp=w*8+16,w*4+12,w*4+20
|
||||
pixels=w*h
|
||||
rgba=struct.pack('<4e',.5,.5,.5,.5)
|
||||
observed=struct.pack('<4e',.125,.125,.125,.75)
|
||||
current=b''.join(rgba*w+b'\xa5'*16 for _ in range(h))
|
||||
color=b''.join(observed*w+b'\xa5'*16 for _ in range(h))
|
||||
depth=b''.join(struct.pack('<'+'f'*w,*[
|
||||
.5 if y==32 else .9 if x<=31 and y<=31 else 1. for x in range(w)])+b'\xa5'*12 for y in range(h))
|
||||
motion=b''.join(bytes(w*4)+b'\xa5'*20 for _ in range(h))
|
||||
input_depth=b''.join(struct.pack('<f',.5)*w+b'\xa5'*12 for _ in range(h))
|
||||
for sparse in (False,True):
|
||||
with self.subTest(sparse=sparse), tempfile.TemporaryDirectory() as temp:
|
||||
directory=Path(temp);out=directory/'out';out.mkdir()
|
||||
arrays=[current,color,depth,motion,color,b'\xff'*pixels,struct.pack('<f',1.)*pixels,
|
||||
b'\xa5'*(pitch*h),b'\xa5'*(pixels+64),b'\xa5'*(pixels*4+64),
|
||||
input_depth,b'\xa5'*(pitch*h)]
|
||||
if sparse:arrays.append(b'\xa5'*(pixels+64))
|
||||
addresses=[(i+1)*0x100000000 for i in range(len(arrays))]
|
||||
args=struct.pack('<17Q5I2fI',*addresses[:12],pitch,dp,mp,pitch,dp,w,h,w,h,0,.25,.25,3)
|
||||
if sparse:args+=struct.pack('<Q',addresses[12])
|
||||
(directory/'args.bin').write_bytes(args)
|
||||
manifest=['kernel '+('d4r_wire_history_sparse' if sparse else 'd4r_wire_history'),
|
||||
'launch 5 5 1 256 1 1 0',f'args {len(args)}']
|
||||
for i,data in enumerate(arrays):
|
||||
(directory/f'alloc-{i}.bin').write_bytes(data)
|
||||
manifest += [f'alloc {i} {hex(addresses[i])} {len(data)}',f'pointer {168 if i==12 else i*8} {i} 0']
|
||||
(directory/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
name='d4r_wire_history_sparse' if sparse else 'd4r_wire_history'
|
||||
subprocess.run([runner,str(kernel),name,str(directory),str(out),'1'],check=True,capture_output=True)
|
||||
if sparse:
|
||||
for i in range(len(arrays)):(directory/f'alloc-{i}.bin').write_bytes((out/f'alloc-{i}.bin').read_bytes())
|
||||
manifest[0]='kernel d4r_wire_history_apply'
|
||||
(directory/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
subprocess.run([runner,str(kernel),'d4r_wire_history_apply',str(directory),str(out),'1'],check=True,capture_output=True)
|
||||
result=(out/f'alloc-{0 if sparse else 7}.bin').read_bytes()
|
||||
# The .0625 wire corner overlaps a different, thick foreground object.
|
||||
self.assertEqual(result[31*pitch+31*8:31*pitch+32*8],rgba)
|
||||
self.assertEqual((out/'alloc-8.bin').read_bytes()[31*w+31],255)
|
||||
# The neighboring actual wire remains repairable; alpha stays untouched.
|
||||
wire=result[32*pitch+32*8:32*pitch+33*8]
|
||||
self.assertNotEqual(wire,rgba)
|
||||
self.assertEqual(wire[6:],rgba[6:])
|
||||
for i in ((1,2,3,4,5,6,10) if sparse else (0,1,2,3,4,5,6,10)):
|
||||
self.assertEqual((out/f'alloc-{i}.bin').read_bytes(),arrays[i])
|
||||
for y in range(h):self.assertEqual(result[y*pitch+w*8:(y+1)*pitch],b'\xa5'*16)
|
||||
for i,end in ((8,pixels),(9,pixels*4)):
|
||||
self.assertEqual((out/f'alloc-{i}.bin').read_bytes()[end:],b'\xa5'*64)
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Sparse wire publication rejects aliased scratch, inputs and overflowing ranges."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class WireHistoryTests(unittest.TestCase):
|
||||
def test_sparse_buffer_contract(self):
|
||||
source = r'''
|
||||
#include "d4r_wire_history.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
int main(void) {
|
||||
D4rWireHistorySparseParams p; memset(&p,0,sizeof(p));
|
||||
D4rWireHistoryParams* b=&p.base;
|
||||
b->width=65;b->height=67;b->render_width=21;b->render_height=22;
|
||||
b->current=0x10000000;b->color=0x20000000;b->native_depth=0x30000000;
|
||||
b->motion=0x40000000;b->previous=0x50000000;b->previous_age=0x60000000;
|
||||
b->previous_depth=0x70000000;b->output=0x80000000;b->output_age=0x90000000;
|
||||
b->output_depth=0xa0000000;b->input_depth=0xb0000000;b->output_history=0xc0000000;
|
||||
b->color_pitch=21*8;b->depth_pitch=b->motion_pitch=65*4;
|
||||
b->output_pitch=65*8;b->input_depth_pitch=21*4;p.patch_mask=0xd0000000;
|
||||
assert(sizeof(p)==176 && d4r_wire_history_sparse_parameters_valid(&p));
|
||||
assert(!d4r_wire_history_sparse_parameters_valid(0));
|
||||
D4rWireHistorySparseParams q=p;q.patch_mask=0;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.patch_mask=UINT64_MAX-16;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.patch_mask=q.base.current+8;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.patch_mask=q.base.previous_age+1;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.base.current=q.base.color;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.base.output=q.base.native_depth+4;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.base.current=q.base.output_history+8;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.base.output_pitch=UINT64_MAX-7;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.base.current=UINT64_MAX-1024;assert(!d4r_wire_history_sparse_parameters_valid(&q));
|
||||
q=p;q.patch_mask=q.base.current+66*q.base.output_pitch+65*8;
|
||||
assert(d4r_wire_history_sparse_parameters_valid(&q));
|
||||
D4rWireHistoryPreparedParams prepared={p,0xe0000000};
|
||||
assert(sizeof(prepared)==184 && d4r_wire_history_prepared_parameters_valid(&prepared));
|
||||
D4rWireHistoryPreparedParams no=prepared;no.axes=0;assert(!d4r_wire_history_prepared_parameters_valid(&no));
|
||||
no=prepared;no.axes=no.sparse.base.current;assert(!d4r_wire_history_prepared_parameters_valid(&no));
|
||||
no=prepared;no.axes=no.sparse.patch_mask;assert(!d4r_wire_history_prepared_parameters_valid(&no));
|
||||
no=prepared;no.axes=UINT64_MAX-16;assert(!d4r_wire_history_prepared_parameters_valid(&no));
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
runner = Path(tmp)/'wire-contract'
|
||||
for compiler, language, standard in (('cc', 'c', 'c11'), ('c++', 'c++', 'c++17')):
|
||||
subprocess.run([compiler, '-x', language, '-std='+standard, '-Wall', '-Wextra', '-Werror',
|
||||
'-I', str(ROOT/'tools'), '-o', str(runner), '-'],
|
||||
input=source, text=True, check=True, capture_output=True)
|
||||
subprocess.run([str(runner)], check=True)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Opt-in GPU checks for wire color, history eligibility, padding and immutable inputs."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('D4R_RUN_GPU_TESTS') == '1', 'opt-in GPU test')
|
||||
class WireHistoryGpuTests(unittest.TestCase):
|
||||
def test_wire_source_history_reset_and_expiry(self):
|
||||
runner = os.environ['D4R_DUMP_RUNNER']
|
||||
kernel = Path(os.environ.get('D4R_WIRE_TEST_KERNEL', str(ROOT / 'build/wine-nvcuda/generated' /
|
||||
(os.environ.get('D4R_GPU_ARCH', 'gfx1101') + '.hsaco'))))
|
||||
w, h = 65, 67 # partial workgroups on both axes
|
||||
pitch, dp, mp = w*8+16, w*4+12, w*4+20
|
||||
pixels = w*h
|
||||
rgba = struct.pack('<4e', .5, .5, .5, .5)
|
||||
observed = struct.pack('<4e', .125, .125, .125, .75)
|
||||
repaired = struct.pack('<4e', .125, .125, .125, .5)
|
||||
current = b''.join(rgba*w + b'\xa5'*16 for _ in range(h))
|
||||
color = b''.join(observed*w + b'\xa5'*16 for _ in range(h))
|
||||
native = b''.join(struct.pack('<'+'f'*w, *[.5 if x == 32 else 1. for x in range(w)]) +
|
||||
b'\xa5'*12 for _ in range(h))
|
||||
motion = b''.join(b'\0'*(w*4) + b'\xa5'*20 for _ in range(h))
|
||||
for sky_only in (False, True):
|
||||
for conservative in (False, True):
|
||||
for sparse in (False, True):
|
||||
for mode in ('source', 'history', 'reset', 'expired', 'disoccluded', 'missing', 'neighbor', 'far', 'backdrop'):
|
||||
for jitter in (0, -1, 1):
|
||||
with self.subTest(mode=mode, jitter=jitter, conservative=conservative, sparse=sparse, sky_only=sky_only), tempfile.TemporaryDirectory() as tmp:
|
||||
directory = Path(tmp); output = directory/'out'; output.mkdir()
|
||||
source = mode in ('source', 'backdrop')
|
||||
history = mode in ('history', 'expired', 'disoccluded', 'neighbor')
|
||||
z = .999988 if mode == 'far' else .5
|
||||
near = b''.join(struct.pack('<'+'f'*w, *[z if x == 32 else .9 if mode == 'backdrop' else 1. for x in range(w)]) + b'\xa5'*12 for _ in range(h))
|
||||
previous = color
|
||||
if mode == 'neighbor':
|
||||
previous = b''.join(b''.join(observed if x == 32-jitter else struct.pack('<4e', .01, .01, .01, .75) for x in range(w)) + b'\xa5'*16 for _ in range(h))
|
||||
input_depth = b''.join(struct.pack('<f', z+1e-6 if mode == 'far' else .5 if source else 1.)*w + b'\xa5'*12 for _ in range(h))
|
||||
arrays = [current, color, near, motion, previous,
|
||||
bytes([(4 if conservative else 16) if mode == 'expired' else (3 if conservative else 15)])*pixels,
|
||||
struct.pack('<f', 0. if mode == 'disoccluded' else .5)*pixels,
|
||||
b'\xa5'*(pitch*h), b'\xa5'*(pixels+64), b'\xa5'*(pixels*4+64),
|
||||
input_depth, b'\xa5'*(pitch*h)]
|
||||
if sparse: arrays.append(b'\xa5'*(pixels+64))
|
||||
addresses = [(i+1)*0x100000000 for i in range(len(arrays))]
|
||||
args = struct.pack('<17Q5I2fI', *addresses[:12], pitch, dp, mp, pitch, dp,
|
||||
w, h, w, h, int(history), jitter, -jitter, int(conservative) | (2 if sky_only else 0))
|
||||
if sparse: args += struct.pack('<Q', addresses[12])
|
||||
(directory/'args.bin').write_bytes(args)
|
||||
manifest = ['kernel '+('d4r_wire_history_sparse' if sparse else 'd4r_wire_history'), 'launch 5 5 1 256 1 1 0', f'args {len(args)}']
|
||||
for i, data in enumerate(arrays):
|
||||
(directory/f'alloc-{i}.bin').write_bytes(data)
|
||||
manifest += [f'alloc {i} {hex(addresses[i])} {len(data)}', f'pointer {168 if i == 12 else i*8} {i} 0']
|
||||
(directory/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
if sparse:
|
||||
subprocess.run([runner, str(kernel), 'd4r_wire_history_sparse', str(directory), str(output), '1'], check=True, capture_output=True)
|
||||
for i in range(len(arrays)):
|
||||
(directory/f'alloc-{i}.bin').write_bytes((output/f'alloc-{i}.bin').read_bytes())
|
||||
manifest[0]='kernel d4r_wire_history_apply'
|
||||
(directory/'manifest.txt').write_text('\n'.join(manifest)+'\n')
|
||||
subprocess.run([runner, str(kernel), 'd4r_wire_history_apply', str(directory), str(output), '1'], check=True, capture_output=True)
|
||||
else:
|
||||
subprocess.run([runner, str(kernel), 'd4r_wire_history', str(directory), str(output), '1'], check=True, capture_output=True)
|
||||
for i in ((1, 2, 3, 4, 5, 6, 10) if sparse else (0, 1, 2, 3, 4, 5, 6, 10)):
|
||||
self.assertEqual((output/f'alloc-{i}.bin').read_bytes(), arrays[i])
|
||||
expected = bytearray(current)
|
||||
cache = bytearray(current)
|
||||
age = bytearray(b'\xff'*pixels + b'\xa5'*64)
|
||||
depth = bytearray(struct.pack('<f', 1.)*pixels + b'\xa5'*64)
|
||||
for y in range(16, h-16):
|
||||
if mode == 'backdrop' and sky_only: continue
|
||||
x = 32-jitter
|
||||
offset, index = y*pitch+x*8, y*w+x
|
||||
depth[index*4:index*4+4] = struct.pack('<f', z)
|
||||
if mode in ('source', 'history', 'neighbor', 'backdrop') or (mode == 'far' and not conservative):
|
||||
value = .01 if mode == 'neighbor' and not conservative else .125
|
||||
expected[offset:offset+8] = struct.pack('<4e', value, value, value, .5)
|
||||
cache[offset:offset+8] = struct.pack('<4e', value, value, value, .75)
|
||||
age[index] = 0 if source or mode == 'far' else 4 if conservative else 16
|
||||
self.assertEqual((output/f'alloc-{0 if sparse else 7}.bin').read_bytes(), expected)
|
||||
self.assertEqual((output/'alloc-8.bin').read_bytes(), age)
|
||||
self.assertEqual((output/'alloc-9.bin').read_bytes(), depth)
|
||||
self.assertEqual((output/'alloc-11.bin').read_bytes(), cache)
|
||||
Reference in new issue
Block a user