Files
d4r/engine/test_game.cpp

405 lines
27 KiB
C++

// Native regression test for direct game textures, rectangle origins and queued descriptor rotation.
// Reuse the standalone replay runner's Vulkan context/readback helpers, without another device harness.
#define main d4r_replay_main
#include "replay.cpp"
#undef main
#include "game.h"
#include "game_m.h"
#include <cmath>
namespace {
constexpr unsigned rotatingMasks[] = {15u, 14u, 13u, 11u, 15u, 8u};
struct TestImage {
Context& context;
VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkFormat format;
VkImageAspectFlags aspect;
uint32_t w, h, channels, bits;
Buffer upload;
TestImage(Context& c, VkFormat f, uint32_t width, uint32_t height, uint32_t nc, uint32_t nb,
VkImageAspectFlags a = VK_IMAGE_ASPECT_COLOR_BIT)
: context(c), format(f), aspect(a), w(width), h(height), channels(nc), bits(nb),
upload(c, VkDeviceSize(w) * h * nc * nb / 8) {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D; info.format = f; info.extent = {w,h,1};
info.mipLevels = info.arrayLayers = 1; info.samples = VK_SAMPLE_COUNT_1_BIT;
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if (f == VK_FORMAT_R16G16B16A16_SFLOAT || f == VK_FORMAT_R32G32B32A32_SFLOAT ||
f == VK_FORMAT_A2B10G10R10_UNORM_PACK32) info.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
CK(vkCreateImage(c.device, &info, nullptr, &image));
VkMemoryRequirements requirements; vkGetImageMemoryRequirements(c.device, image, &requirements);
memory = c.allocate(requirements, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
CK(vkBindImageMemory(c.device, image, memory, 0));
VkImageViewCreateInfo vi{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
vi.image = image; vi.viewType = VK_IMAGE_VIEW_TYPE_2D; vi.format = f;
vi.subresourceRange = {a,0,1,0,1}; CK(vkCreateImageView(c.device, &vi, nullptr, &view));
}
~TestImage() {
if (view) vkDestroyImageView(context.device, view, nullptr);
if (image) vkDestroyImage(context.device, image, nullptr);
if (memory) vkFreeMemory(context.device, memory, nullptr);
}
void put(uint32_t x, uint32_t y, uint32_t channel, float value) {
const size_t i = (size_t(y)*w+x)*channels+channel;
if (bits == 32) static_cast<float*>(upload.map)[i] = value;
else static_cast<_Float16*>(upload.map)[i] = static_cast<_Float16>(value);
}
void initialize(VkImageLayout layout) {
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; b.image = image;
b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b.newLayout = VK_IMAGE_LAYOUT_GENERAL;
b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.subresourceRange = {aspect,0,1,0,1};
vkCmdPipelineBarrier(context.command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0,0,nullptr,0,nullptr,1,&b);
auto r = region(w,h); r.imageSubresource.aspectMask = aspect;
vkCmdCopyBufferToImage(context.command, upload.handle, image, VK_IMAGE_LAYOUT_GENERAL, 1, &r);
if (layout != VK_IMAGE_LAYOUT_GENERAL) {
b.oldLayout = VK_IMAGE_LAYOUT_GENERAL; b.newLayout = layout;
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(context.command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
0,0,nullptr,0,nullptr,1,&b);
}
}
d4r::GameImage game(uint32_t x, uint32_t y, bool direct, VkImageLayout layout) const {
return {image,format,layout,aspect,x,y,direct ? view : VK_NULL_HANDLE};
}
};
struct Case {
uint32_t ow = 1283, oh = 723;
bool depthAspect = false, wideColor = false, wideMotion = false, wideOutput = false, readonly = false;
bool rgb10Output = false, rotateOutputFormats = false;
bool regularDepth = false, swapDepth = false; // K: depth convention; near and far values exchanged
float exposureScale = 1, preExposure = 1; // K: the game's factors on the measured exposure
bool dynamicM = false, largerCapacityM = false, explicitSizeResetM = false;
bool displayMotion = false; // K: motion vectors at output resolution, in output pixels
bool gameExposure = false; float exposureValue = 1; // K: the game's 1x1 exposure texture instead of measurement
};
void validateFrames(const Buffer& results, VkDeviceSize stride, const std::array<std::unique_ptr<TestImage>, 3>& output, unsigned count) {
for (unsigned frame = 0; frame < count; ++frame) {
const auto& image = *output[frame % output.size()];
if (image.format == VK_FORMAT_A2B10G10R10_UNORM_PACK32) continue;
const auto* data = static_cast<const char*>(results.map) + frame * stride;
for (VkDeviceSize i = 0; i < image.upload.size / (image.bits / 8); ++i)
if (!std::isfinite(image.bits == 32 ? reinterpret_cast<const float*>(data)[i] : float(reinterpret_cast<const _Float16*>(data)[i])))
throw std::runtime_error("nonfinite adapter output");
}
}
std::vector<char> evaluate(Context& c, d4r::Model& model, const Case& test, unsigned mask,
bool queued, bool bareEngine = false, bool rotateInputs = false) {
constexpr uint32_t rw = 640, rh = 360, count = 6, slots = 3;
std::array<std::unique_ptr<TestImage>, slots> color, motion, depth, output;
TestImage exposureTexture(c, VK_FORMAT_R32_SFLOAT, 1, 1, 1, 32);
exposureTexture.put(0,0,0,test.exposureValue);
const auto inputLayout = test.readonly ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL;
for (unsigned slot = 0; slot < slots; ++slot) {
color[slot] = std::make_unique<TestImage>(c, test.wideColor ? VK_FORMAT_R32G32B32A32_SFLOAT : VK_FORMAT_R16G16B16A16_SFLOAT, rw+16,rh+16,4,test.wideColor ? 32 : 16);
const uint32_t mw = test.displayMotion ? test.ow+16 : rw+16, mh = test.displayMotion ? test.oh+16 : rh+16;
motion[slot] = std::make_unique<TestImage>(c, test.wideMotion ? VK_FORMAT_R32G32_SFLOAT : VK_FORMAT_R16G16_SFLOAT, mw,mh,2,test.wideMotion ? 32 : 16);
depth[slot] = std::make_unique<TestImage>(c, test.depthAspect ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_R32_SFLOAT, rw+16,rh+16,1,32,
test.depthAspect ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT);
const bool packed = test.rgb10Output && (!test.rotateOutputFormats || slot != 1);
output[slot] = std::make_unique<TestImage>(c, packed ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 :
test.wideOutput ? VK_FORMAT_R32G32B32A32_SFLOAT : VK_FORMAT_R16G16B16A16_SFLOAT,
test.ow+16,test.oh+16,packed ? 1 : 4,packed || test.wideOutput ? 32 : 16);
for (uint32_t y = 0; y < rh+16; ++y) for (uint32_t x = 0; x < rw+16; ++x) {
const int cx = int(x)-7, cy = int(y)-9;
const bool inside = cx >= 0 && cy >= 0 && cx < int(rw) && cy < int(rh);
for (unsigned ch = 0; ch < 4; ++ch) color[slot]->put(x,y,ch, ch == 3 ? 1.f :
inside ? .1f + ((cx + 3*cy + int(slot)*13 + int(ch)*7)%19)*.08f + (test.wideColor ? .00012345f : 0) : 20.f);
depth[slot]->put(x,y,0, (((int(x)-11)/13)%2 != 0) != test.swapDepth ? .2f : .8f);
}
for (uint32_t y = 0; y < mh; ++y) for (uint32_t x = 0; x < mw; ++x) {
const int mx = int(x)-3, my = int(y)-5;
motion[slot]->put(x,y,0, .25f * ((mx+int(slot))%7-3) + (test.wideMotion ? .00012345f : 0));
motion[slot]->put(x,y,1, .25f * ((my+int(slot))%9-4) + (test.wideMotion ? .00032109f : 0));
}
std::memset(output[slot]->upload.map,0,output[slot]->upload.size);
}
// The borrowed views above outlive both the adapter/engine and their immutable descriptor cache.
d4r::GameUpscaler adapter(model,c.physical,c.device,test.ow,test.oh,rw,rh);
auto shape = d4r::makeKShape(test.ow,test.oh);
std::unique_ptr<d4r::Engine> engine;
if (bareEngine) engine = std::make_unique<d4r::Engine>(model,shape);
const auto bytes = std::max({output[0]->upload.size, output[1]->upload.size, output[2]->upload.size});
Buffer results(c,bytes*count);
std::memset(results.map,0,results.size);
c.begin();
for (unsigned slot = 0; slot < slots; ++slot) {
color[slot]->initialize(inputLayout); motion[slot]->initialize(inputLayout);
depth[slot]->initialize(inputLayout); output[slot]->initialize(VK_IMAGE_LAYOUT_GENERAL);
}
exposureTexture.initialize(inputLayout);
if (engine) engine->recordInitialize(c.command);
c.submit();
if (queued) c.begin();
for (unsigned frame = 0; frame < count; ++frame) {
unsigned slot = frame%slots;
if (!queued) c.begin();
const unsigned frameMask = rotateInputs ? rotatingMasks[frame] : mask;
d4r::GameFrame f;
f.color = color[slot]->game(7,9,frameMask&1,inputLayout);
f.motion = motion[slot]->game(3,5,frameMask&2,inputLayout);
f.depth = depth[slot]->game(11,13,frameMask&4,inputLayout);
f.output = output[slot]->game(5,7,frameMask&8,VK_IMAGE_LAYOUT_GENERAL);
if (test.gameExposure) f.exposure = exposureTexture.game(0,0,true,inputLayout);
f.settings.renderWidth = rw-frame%2*8; f.settings.renderHeight = rh-frame%2*4;
f.settings.reset = frame == 0 || frame == 4; f.settings.autoExposure = !test.gameExposure;
f.settings.depthInverted = !test.regularDepth; f.settings.exposureScale = test.exposureScale; f.settings.preExposure = test.preExposure;
f.settings.displayMotion = test.displayMotion;
f.settings.jitter[0] = frame%2 ? .25f : -.125f; f.settings.jitter[1] = frame%3 ? .375f : -.25f;
if (engine) {
auto p = d4r::makeKFrameParams(shape,f.settings);
const int origins[3][2] = {{7,9},{3,5},{11,13}};
int32_t* in[] = {p.input.colOrigin,p.input.mvOrigin,p.input.depOrigin};
int32_t* out[] = {p.output.colOrigin,p.output.mvOrigin,p.output.depOrigin};
int32_t* hi[] = {p.output.colMax,p.output.mvMax,p.output.depMax};
for (unsigned k=0;k<3;++k) for(unsigned a=0;a<2;++a) {in[k][a]=out[k][a]=origins[k][a];hi[k][a]+=origins[k][a];}
p.output.outOrigin[0]=5;p.output.outOrigin[1]=7;
d4r::ExposureControl exposure; exposure.origin[0]=7;exposure.origin[1]=9;
exposure.size[0]=int32_t(f.settings.renderWidth);exposure.size[1]=int32_t(f.settings.renderHeight);
exposure.measure=frame%2==0||f.settings.reset;exposure.reset=f.settings.reset;
exposure.exposureScale=test.exposureScale;exposure.preExposure=test.preExposure;
exposure.game=test.gameExposure;
auto windows=d4r::makeKWindows(shape,frame);
engine->setFrame({f.color.view,f.motion.view,f.depth.view,inputLayout,inputLayout,inputLayout,f.exposure.view,inputLayout},p.input,p.output,&windows,&exposure,{f.output.view,f.output.format});
engine->recordFrame(c.command);
} else adapter.record(c.command,f);
auto r=region(output[slot]->w,output[slot]->h);r.bufferOffset=frame*bytes;
vkCmdCopyImageToBuffer(c.command,output[slot]->image,VK_IMAGE_LAYOUT_GENERAL,results.handle,1,&r);
if (!queued) {
barrier(c.command,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_HOST_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_HOST_READ_BIT);
c.submit();
}
}
if (queued) {
barrier(c.command,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_HOST_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_HOST_READ_BIT);
c.submit();
}
validateFrames(results,bytes,output,count);
return {static_cast<char*>(results.map),static_cast<char*>(results.map)+results.size};
}
std::vector<char> evaluateM(Context& c, d4r::MModel& model, const Case& test, unsigned mask, bool queued, bool rotateInputs = false) {
constexpr uint32_t rw = 640, rh = 360, ow = 960, oh = 540, count = 6, slots = 3;
std::array<std::unique_ptr<TestImage>, slots> color, motion, depth, output;
const auto layout = test.readonly ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL;
for (unsigned slot = 0; slot < slots; ++slot) {
color[slot] = std::make_unique<TestImage>(c, VK_FORMAT_R16G16B16A16_SFLOAT, rw,rh,4,16);
motion[slot] = std::make_unique<TestImage>(c, VK_FORMAT_R16G16_SFLOAT, rw,rh,2,16);
depth[slot] = std::make_unique<TestImage>(c, test.depthAspect ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_R32_SFLOAT, rw,rh,1,32,
test.depthAspect ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT);
const bool packed = test.rgb10Output && (!test.rotateOutputFormats || slot != 1);
output[slot] = std::make_unique<TestImage>(c, packed ? VK_FORMAT_A2B10G10R10_UNORM_PACK32 :
test.wideOutput ? VK_FORMAT_R32G32B32A32_SFLOAT : VK_FORMAT_R16G16B16A16_SFLOAT,
ow,oh,packed ? 1 : 4,packed || test.wideOutput ? 32 : 16);
for (uint32_t y = 0; y < rh; ++y) for (uint32_t x = 0; x < rw; ++x) {
for (unsigned ch = 0; ch < 4; ++ch)
color[slot]->put(x,y,ch,ch == 3 ? 1.f : .1f + ((x+3*y+slot*13+ch*7)%19)*.08f);
motion[slot]->put(x,y,0,.25f*(int((x+slot)%7)-3));
motion[slot]->put(x,y,1,.25f*(int((y+slot)%9)-4));
depth[slot]->put(x,y,0,(x/13)%2 ? .2f : .8f);
}
std::memset(output[slot]->upload.map,0,output[slot]->upload.size);
}
d4r::GameUpscalerM adapter(model,c.physical,c.device,ow,oh,test.largerCapacityM ? ow : rw,test.largerCapacityM ? oh : rh);
const auto bytes = std::max({output[0]->upload.size, output[1]->upload.size, output[2]->upload.size});
Buffer results(c,bytes*count);
std::memset(results.map,0,results.size);
c.begin();
for (unsigned slot = 0; slot < slots; ++slot) {
color[slot]->initialize(layout); motion[slot]->initialize(layout);
depth[slot]->initialize(layout); output[slot]->initialize(VK_IMAGE_LAYOUT_GENERAL);
}
c.submit();
if (queued) c.begin();
for (unsigned frame = 0; frame < count; ++frame) {
const unsigned slot = frame%slots;
if (!queued) c.begin();
d4r::GameFrameM f;
const unsigned frameMask = rotateInputs ? rotatingMasks[frame] : mask;
f.color=color[slot]->game(0,0,frameMask&1,layout); f.motion=motion[slot]->game(0,0,frameMask&2,layout);
f.depth=depth[slot]->game(0,0,frameMask&4,layout); f.output=output[slot]->game(0,0,frameMask&8,VK_IMAGE_LAYOUT_GENERAL);
if (test.dynamicM) {
const uint32_t widths[count] = {640, 608, 448, 448, 640, 640};
const uint32_t heights[count] = {360, 344, 256, 256, 360, 360};
f.renderWidth = widths[frame]; f.renderHeight = heights[frame];
}
f.reset=frame==0||frame==4||(test.explicitSizeResetM && (frame==1||frame==2));
f.jitter[0]=frame%2 ? .25f : -.125f; f.jitter[1]=frame%3 ? .375f : -.25f;
adapter.record(c.command,f);
auto r=region(ow,oh); r.bufferOffset=frame*bytes;
vkCmdCopyImageToBuffer(c.command,output[slot]->image,VK_IMAGE_LAYOUT_GENERAL,results.handle,1,&r);
if (!queued) {
barrier(c.command,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_HOST_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_HOST_READ_BIT);
c.submit();
}
}
if (queued) {
barrier(c.command,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_HOST_BIT,VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_HOST_READ_BIT);
c.submit();
}
validateFrames(results,bytes,output,count);
return {static_cast<char*>(results.map),static_cast<char*>(results.map)+results.size};
}
void expectEqual(const std::vector<char>& expected,const std::vector<char>& actual,const std::string& name) {
if(expected!=actual) throw std::runtime_error(name+": output mismatch");
std::cout<<name<<": byte-identical (six temporal frames)\n";
}
} // namespace
// Normalized bilinear sampling can round differently when the allocation extent changes.
// FP8 quantization can amplify a rounding difference in a few recurrent pixels.
// Require less than 1% per-channel error and at least 86 dB PSNR at peak 1.
void expectCapacityEquivalent(const std::vector<char>& expected, const std::vector<char>& actual, const std::string& name) {
if (expected.size() != actual.size()) throw std::runtime_error(name + ": size mismatch");
double squared = 0; float relative = 0;
for (size_t i = 0; i < expected.size()/2; ++i) {
const float a = float(reinterpret_cast<const _Float16*>(expected.data())[i]);
const float b = float(reinterpret_cast<const _Float16*>(actual.data())[i]);
const float error = std::abs(a-b);
if (!std::isfinite(error)) throw std::runtime_error(name + ": nonfinite output");
squared += double(error)*error;
relative = std::max(relative,error/std::max(1.f,std::abs(a)));
}
const double rmse = std::sqrt(squared/(expected.size()/2));
if (relative > .01f || rmse > 5e-5)
throw std::runtime_error(name + ": capacity changed the result (relative " + std::to_string(relative) + ", RMSE " + std::to_string(rmse) + ")");
std::cout<<name<<": RMSE "<<rmse<<", max relative error "<<relative<<"\n";
}
void checkDynamicM(Context& c, d4r::MModel& m) {
Case dynamic; dynamic.dynamicM = true;
const auto reference = evaluateM(c,m,dynamic,0,false);
expectEqual(reference,evaluateM(c,m,dynamic,15,true),"M/L dynamic sizes with queued direct inputs");
dynamic.explicitSizeResetM = true;
expectEqual(reference,evaluateM(c,m,dynamic,0,true),"M/L size changes reset history automatically");
dynamic.explicitSizeResetM = false;
dynamic.largerCapacityM = true;
expectCapacityEquivalent(reference,evaluateM(c,m,dynamic,0,true),"M/L dynamic sizes within output-sized capacity");
expectCapacityEquivalent(reference,evaluateM(c,m,dynamic,15,true),"M/L dynamic sizes with larger capacity and direct inputs");
}
int main(int argc,char** argv) {
try {
if (argc == 3 && !std::strcmp(argv[1], "--dynamic-ml")) {
Context c; c.create(2,true);
d4r::MModel m(c.physical,c.device,argv[2],c.cache);
c.begin(); m.recordUpload(c.command); c.submit();
checkDynamicM(c,m);
return 0;
}
if(argc!=2 && argc!=3) throw std::runtime_error("usage: test-game K_MODEL_DIR [M_MODEL_DIR] | --dynamic-ml M_OR_L_MODEL_DIR");
// The reconstruction table NGX itself selected at these sizes (measured through its launch arguments),
// on both sides of every threshold and for unequal axis ratios: {render w, h, output w, h, table}.
{
const uint32_t probes[][5] = {{1280,720,1280,720,4},{1238,696,1280,720,4},{1232,693,1280,720,6},{1030,580,1280,720,6},
{1024,576,1280,720,8},{880,495,1280,720,8},{872,491,1280,720,10},{764,430,1280,720,10},{760,428,1280,720,12},
{680,383,1280,720,12},{675,380,1280,720,14},{426,240,1280,720,14},{640,720,1280,720,8},{640,648,1280,720,10},
{640,432,1280,720,12},{1706,960,2560,1440,10},{1280,720,3840,2160,14}};
for (const auto& q : probes)
if (d4r::kReconstructionTable(q[2],q[3],q[0],q[1]) != q[4])
throw std::runtime_error("reconstruction table selection differs from NGX at render "+std::to_string(q[0])+"x"+std::to_string(q[1]));
std::cout<<"reconstruction table selection matches NGX at "<<std::size(probes)<<" measured sizes\n";
}
Context c;c.create(2,argc==3);d4r::Model model(c.physical,c.device,argv[1],c.cache);
if (!model.hasDirectOrigins()) throw std::runtime_error("recompile the model with compile_k.py before testing direct rectangles");
c.begin();model.recordUpload(c.command);c.submit();
for(bool depthAspect:{false,true}) {
Case test;test.depthAspect=depthAspect;
auto reference=evaluate(c,model,test,0,false);
for(unsigned mask:{1u,2u,4u,8u,7u,15u}) expectEqual(reference,evaluate(c,model,test,mask,true),"direct mask "+std::to_string(mask)+(depthAspect?" D32":" R32"));
for (bool queued : {false, true})
expectEqual(reference,evaluate(c,model,test,15,queued,false,true),"late fallback allocation "+std::string(queued ? "queued" : "submitted")+(depthAspect ? " D32" : " R32"));
if (!model.hasRgb10Output()) throw std::runtime_error("recompile the K model for RGB10A2 checks");
for (bool mixed : {false, true}) {
test.rgb10Output=true;test.rotateOutputFormats=mixed;
const auto packedReference=evaluate(c,model,test,0,false);
for (bool queued : {false,true})
expectEqual(packedReference,evaluate(c,model,test,15,queued,false,true),
std::string(mixed ? "mixed RGBA16F/RGB10A2" : "RGB10A2")+ " late fallback "+(queued ? "queued" : "submitted")+(depthAspect ? " D32" : " R32"));
}
test.rgb10Output=false;test.rotateOutputFormats=false;
test.readonly=true;
expectEqual(reference,evaluate(c,model,test,15,true),"read-only input layouts");
test.readonly=false;test.wideOutput=true;
expectEqual(evaluate(c,model,test,0,false),evaluate(c,model,test,15,true),"RGBA32F output conversion fallback");
}
// Regular depth picks the nearest neighbour as the smallest depth. With near and far exchanged it must
// choose exactly the texels inverted depth chose; inverted depth on the exchanged values must not.
if (!model.hasRegularDepth()) throw std::runtime_error("recompile the K model for regular-depth checks");
{
Case test;
const auto reference=evaluate(c,model,test,0,false);
test.swapDepth=true;
if (evaluate(c,model,test,0,false)==reference) throw std::runtime_error("depth does not affect the output; the regular-depth check is vacuous");
test.regularDepth=true;
for (unsigned mask:{0u,15u}) expectEqual(reference,evaluate(c,model,test,mask,mask!=0),"regular depth mirrors inverted depth, direct mask "+std::to_string(mask));
}
// The game's exposure scale multiplies the measured exposure: the adapter must equal an Engine given
// the scaled key, and differ from the neutral result.
{
Case test;test.wideColor=true;test.wideMotion=true;test.readonly=true;
const auto neutral=evaluate(c,model,test,15,true);
test.exposureScale=.7f;test.preExposure=3.f;
const auto scaled=evaluate(c,model,test,15,true);
if (scaled==neutral) throw std::runtime_error("the exposure scale and pre-exposure do not affect the output");
expectEqual(evaluate(c,model,test,15,false,true),scaled,"exposure scale 0.7 and pre-exposure 3 match the Engine");
}
// A game exposure texture replaces the measurement (AutoExposure off): the adapter must equal an Engine given
// the same value, and the value must reach the output. A non-positive texel is read as 1.
if (!model.hasGameExposure()) throw std::runtime_error("recompile the K model for game exposure checks");
{
Case test;test.wideColor=true;test.wideMotion=true;test.readonly=true;test.gameExposure=true;test.exposureValue=.6f;
const auto game=evaluate(c,model,test,15,true);
expectEqual(evaluate(c,model,test,15,false,true),game,"game exposure 0.6 matches the Engine");
test.exposureValue=1.f;
const auto unit=evaluate(c,model,test,15,true);
if (game==unit) throw std::runtime_error("the game exposure does not affect the output");
test.exposureValue=0.f;
expectEqual(evaluate(c,model,test,15,true),unit,"non-positive game exposure reads as 1");
}
Case precise;precise.wideColor=true;precise.wideMotion=true;precise.readonly=true;
expectEqual(evaluate(c,model,precise,15,false,true),evaluate(c,model,precise,15,true),"RGBA32F color/RG32F motion match unquantized Engine");
// Display-resolution motion (flag 4): output-sized vectors in output pixels. The render-resolution
// result must differ, or the check is vacuous. The direct/copy comparisons use the same input formats on
// both sides (a wide source may only be borrowed, not copied: the fallback copy quantizes to RGBA16F/RG16F).
if (!model.hasDisplayMotion()) throw std::runtime_error("recompile the K model with compile_k.py for display-resolution motion checks");
{
Case display;display.displayMotion=true;
const auto reference=evaluate(c,model,display,0,false);
Case render;
if (evaluate(c,model,render,0,false)==reference) throw std::runtime_error("display-resolution motion does not affect the output; the check is vacuous");
for (unsigned mask:{2u,15u}) expectEqual(reference,evaluate(c,model,display,mask,true),"display-resolution motion direct mask "+std::to_string(mask));
expectEqual(reference,evaluate(c,model,display,15,false,true),"display-resolution motion matches the Engine");
Case wide;wide.displayMotion=true;wide.wideColor=true;wide.wideMotion=true;wide.readonly=true;
expectEqual(evaluate(c,model,wide,15,false,true),evaluate(c,model,wide,15,true),"display-resolution motion RGBA32F/RG32F match unquantized Engine");
}
if (argc==3) {
d4r::MModel m(c.physical,c.device,argv[2],c.cache);
c.begin();m.recordUpload(c.command);c.submit();
checkDynamicM(c, m);
for (bool depthAspect:{false,true}) {
Case test;test.depthAspect=depthAspect;
auto reference=evaluateM(c,m,test,0,false);
for (unsigned mask:{1u,2u,4u,8u,7u,15u})
expectEqual(reference,evaluateM(c,m,test,mask,true),"M Quality direct mask "+std::to_string(mask)+(depthAspect?" D32":" R32"));
for (bool queued : {false, true})
expectEqual(reference,evaluateM(c,m,test,15,queued,true),"M late fallback allocation "+std::string(queued ? "queued" : "submitted")+(depthAspect ? " D32" : " R32"));
if (!m.hasRgb10Output()) throw std::runtime_error("recompile the M model for RGB10A2 checks");
for (bool mixed : {false,true}) {
test.rgb10Output=true;test.rotateOutputFormats=mixed;
const auto packedReference=evaluateM(c,m,test,0,false);
for (bool queued : {false,true})
expectEqual(packedReference,evaluateM(c,m,test,15,queued,true),
std::string(mixed ? "M mixed RGBA16F/RGB10A2" : "M RGB10A2")+" late fallback "+(queued ? "queued" : "submitted")+(depthAspect ? " D32" : " R32"));
}
test.rgb10Output=false;test.rotateOutputFormats=false;
test.readonly=true;
expectEqual(reference,evaluateM(c,m,test,15,true),"M read-only input layouts");
test.readonly=false;test.wideOutput=true;
expectEqual(evaluateM(c,m,test,0,false),evaluateM(c,m,test,15,true),"M RGBA32F output conversion fallback");
}
}
std::cout<<"All direct-texture checks passed\n";
return 0;
}catch(const std::exception& e){std::cerr<<"test-game: "<<e.what()<<'\n';return 1;}
}