diff --git a/.github/workflows/source-regressions.yml b/.github/workflows/source-regressions.yml index d943751f..4c53b113 100644 --- a/.github/workflows/source-regressions.yml +++ b/.github/workflows/source-regressions.yml @@ -28,6 +28,8 @@ on: - tests/test_dispatch_clock.py - gpu/vk/vk_framegen.cpp - tests/test_framegen_gate.py + - tests/test_upscale_independent.py + - tests/upscale_independent_test.cpp - gpu/vk/vk_draw.cpp - tests/test_table_track.py - tests/test_desc_clock.py @@ -99,6 +101,8 @@ on: - tests/test_dispatch_clock.py - gpu/vk/vk_framegen.cpp - tests/test_framegen_gate.py + - tests/test_upscale_independent.py + - tests/upscale_independent_test.cpp - gpu/vk/vk_draw.cpp - tests/test_table_track.py - tests/test_desc_clock.py @@ -200,6 +204,9 @@ jobs: - name: Check frame generation stays off until asked if: runner.os == 'Linux' run: python -I -B tests/test_framegen_gate.py + - name: Check upscaling works without frame generation + if: runner.os == 'Linux' + run: python -I -B tests/test_upscale_independent.py - name: Check the table-entry tracker stops looking if: runner.os == 'Linux' run: python -I -B tests/test_table_track.py diff --git a/gpu/vk/vk_framegen.cpp b/gpu/vk/vk_framegen.cpp index eb8a8141..0d0931e6 100644 --- a/gpu/vk/vk_framegen.cpp +++ b/gpu/vk/vk_framegen.cpp @@ -57,11 +57,7 @@ struct Pipe { struct State { bool init = false, failed = false; uint32_t w = 0, h = 0; - Pipe copy, luma, flow, median, synth, scale; - Img cropped; - uint32_t cropped_w = 0, cropped_h = 0; - Img scaled; // (the Insert menu's upscaling: at the window's size) - uint32_t scaled_w = 0, scaled_h = 0; + Pipe copy, luma, flow, median, synth; VkSampler nearest = VK_NULL_HANDLE, linear = VK_NULL_HANDLE; Img frames[2], lumas[2], flows[2]; OutSet sets[3]; // (one shown, one waiting its place on the line, one being made: the pacer never holds the game back) @@ -77,6 +73,15 @@ struct State { int mult = 0; // (the multiplier the sets' frames were made for) } fg; +// Used only by present_image, whose lock serializes direct and interpolated presents. +struct ScaleState { + bool init = false, failed = false; + Pipe pipe; + VkSampler linear = VK_NULL_HANDLE; + Img cropped, scaled; + uint32_t cropped_w = 0, cropped_h = 0, scaled_w = 0, scaled_h = 0; +} scale; + int64_t now_ns() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } @@ -130,8 +135,7 @@ bool init() { !make_pipe(fg.luma, FG_LUMA_COMP_SPV, sizeof FG_LUMA_COMP_SPV, {S, I}, 0) || !make_pipe(fg.flow, FG_FLOW_COMP_SPV, sizeof FG_FLOW_COMP_SPV, {S, S, I}, 0, &spec) || !make_pipe(fg.median, FG_MEDIAN_COMP_SPV, sizeof FG_MEDIAN_COMP_SPV, {S, I}, 0) || - !make_pipe(fg.synth, FG_SYNTH_COMP_SPV, sizeof FG_SYNTH_COMP_SPV, {S, S, S, I}, 4) || - !make_pipe(fg.scale, FG_SCALE_COMP_SPV, sizeof FG_SCALE_COMP_SPV, {S, I}, 8)) + !make_pipe(fg.synth, FG_SYNTH_COMP_SPV, sizeof FG_SYNTH_COMP_SPV, {S, S, S, I}, 4)) return false; VkSamplerCreateInfo si{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; @@ -144,6 +148,28 @@ bool init() { return true; } +bool init_scale() { + if (scale.init) return !scale.failed; + scale.init = scale.failed = true; + if (!dev.push_descriptor || !vkCmdPushDescriptorSetKHR) return false; + const VkDescriptorType S = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, I = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + if (make_pipe(scale.pipe, FG_SCALE_COMP_SPV, sizeof FG_SCALE_COMP_SPV, {S, I}, 8)) { + VkSamplerCreateInfo si{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + si.maxLod = 0.25f; + si.magFilter = si.minFilter = VK_FILTER_LINEAR; + if (vkCreateSampler(dev.device, &si, nullptr, &scale.linear) == VK_SUCCESS) { + scale.failed = false; + return true; + } + } + if (scale.pipe.pipe) vkDestroyPipeline(dev.device, scale.pipe.pipe, nullptr); + if (scale.pipe.layout) vkDestroyPipelineLayout(dev.device, scale.pipe.layout, nullptr); + if (scale.pipe.set) vkDestroyDescriptorSetLayout(dev.device, scale.pipe.set, nullptr); + scale.pipe = {}; + return false; +} + bool make_img(Img& i, uint32_t w, uint32_t h, VkFormat f, VkImageUsageFlags usage) { VkImageCreateInfo ci{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; ci.imageType = VK_IMAGE_TYPE_2D; @@ -322,45 +348,64 @@ bool framegen_on() { VkImage framegen_scale(VkCommandBuffer cmd, VkImageView src, VkImage image, uint32_t sw, uint32_t sh, const int crop[4], uint32_t dw, uint32_t dh) { if (!opts.upscale && !opts.sharpness) return VK_NULL_HANDLE; - if (!src || !fg.scale.pipe) return VK_NULL_HANDLE; + if (!src || !image || !sw || !sh || !dw || !dh) return VK_NULL_HANDLE; + if (crop && crop[2] > 0 && crop[3] > 0 && + (crop[0] < 0 || crop[1] < 0 || (uint32_t)crop[0] >= sw || (uint32_t)crop[1] >= sh)) return VK_NULL_HANDLE; + if (!init_scale()) return VK_NULL_HANDLE; // (the game draws into part of its window textures, 2048 x 1152 of 2560 x 1440 here: the filter was given the whole texture, and the // picture came out in the top left corner of the window with black around it. The crop first, then the filter) if (crop && crop[2] > 0 && crop[3] > 0 && (crop[0] || crop[1] || (uint32_t)crop[2] < sw || (uint32_t)crop[3] < sh)) { const uint32_t cw = std::min((uint32_t)crop[2], sw - (uint32_t)crop[0]), ch = std::min((uint32_t)crop[3], sh - (uint32_t)crop[1]); - if (fg.cropped_w != cw || fg.cropped_h != ch) { + if (scale.cropped_w != cw || scale.cropped_h != ch) { { std::lock_guard qk(dev.present_mu); queue_flush_locked(); vkDeviceWaitIdle(dev.device); } - drop_img(fg.cropped); - if (!make_img(fg.cropped, cw, ch, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) return VK_NULL_HANDLE; - fg.cropped_w = cw, fg.cropped_h = ch; - to_general(cmd, fg.cropped.image); + drop_img(scale.cropped); + scale.cropped_w = scale.cropped_h = 0; + if (!make_img(scale.cropped, cw, ch, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) { + drop_img(scale.cropped); + return VK_NULL_HANDLE; + } + scale.cropped_w = cw, scale.cropped_h = ch; + to_general(cmd, scale.cropped.image); } VkImageBlit b{}; b.srcSubresource = b.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; b.srcOffsets[0] = {crop[0], crop[1], 0}; b.srcOffsets[1] = {crop[0] + (int)cw, crop[1] + (int)ch, 1}; b.dstOffsets[1] = {(int)cw, (int)ch, 1}; - vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_GENERAL, fg.cropped.image, VK_IMAGE_LAYOUT_GENERAL, 1, &b, VK_FILTER_NEAREST); + vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_GENERAL, scale.cropped.image, VK_IMAGE_LAYOUT_GENERAL, 1, &b, VK_FILTER_NEAREST); full_barrier(cmd); - src = fg.cropped.view; + src = scale.cropped.view; } - if (fg.scaled_w != dw || fg.scaled_h != dh) { + if (scale.scaled_w != dw || scale.scaled_h != dh) { { std::lock_guard qk(dev.present_mu); // (the queue is idle only while nothing else submits to it) queue_flush_locked(); vkDeviceWaitIdle(dev.device); } - drop_img(fg.scaled); - if (!make_img(fg.scaled, dw, dh, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) return VK_NULL_HANDLE; - fg.scaled_w = dw, fg.scaled_h = dh; - to_general(cmd, fg.scaled.image); + drop_img(scale.scaled); + scale.scaled_w = scale.scaled_h = 0; + if (!make_img(scale.scaled, dw, dh, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) { + drop_img(scale.scaled); + return VK_NULL_HANDLE; + } + scale.scaled_w = dw, scale.scaled_h = dh; + to_general(cmd, scale.scaled.image); } const struct { int mode; float sharp; } pc{opts.upscale, opts.sharpness / 100.0f}; - pass(cmd, fg.scale, {{src, fg.linear}, {fg.scaled.view, VK_NULL_HANDLE}}, dw, dh, &pc, sizeof pc); - return fg.scaled.image; + pass(cmd, scale.pipe, {{src, scale.linear}, {scale.scaled.view, VK_NULL_HANDLE}}, dw, dh, &pc, sizeof pc); + static const bool report = getenv("RX_VK_UPSCALE_LOG") != nullptr; + static bool reported = false; + if (report && !reported) { + reported = true; + logf("vk: upscale dispatched: mode %d, sharpness %d, source %ux%u, crop request %d,%d %dx%d, destination %ux%u", + opts.upscale, opts.sharpness, sw, sh, crop ? crop[0] : 0, crop ? crop[1] : 0, + crop ? crop[2] : (int)sw, crop ? crop[3] : (int)sh, dw, dh); + } + return scale.scaled.image; } VkImage framegen_made_image(uint32_t* w, uint32_t* h) { diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index 59a6eeb6..ac618efe 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -2261,6 +2261,7 @@ void do_present(const nvn_tex* t, const int crop[4]) { static const char* shots = getenv("RX_VK_SHOTS"); static const int shot_every = getenv("RX_VK_SHOT_EVERY") ? std::max(1, atoi(getenv("RX_VK_SHOT_EVERY"))) : 300; VkImage src = VK_NULL_HANDLE; + VkImageView direct_view = VK_NULL_HANDLE; uint32_t sw = 0, sh = 0; bool shot = false, interpolated = false; VkImage made_src = VK_NULL_HANDLE; @@ -2283,6 +2284,12 @@ void do_present(const nvn_tex* t, const int crop[4]) { t ? t->d.width : 0, t ? t->d.height : 0, t ? t->d.format : 0, tex ? (tex->gpu_written ? "rendered" : "never rendered") : "no image"); if (tex) src = tex->image, sw = tex->width, sh = tex->height; + if (tex && !dev.offscreen && (opts.upscale || opts.sharpness)) { + // Keep the sampled view with the same cached image as the direct present, before advancing its age. + direct_view = texture_view(tex, t, nullptr, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Sample); + tex->last_frame = texture_frame_no(); + tex->last_value = std::max(tex->last_value, value); + } // (frame interpolation: made frames and this one shown by the display thread, vk_framegen.cpp. Recorded // after the submit that signals the present's value, in a submit of its own: the game waits for that value // before it submits its next frame, and the interpolation's GPU work (~1 ms) held it back. The display @@ -2567,7 +2574,7 @@ void do_present(const nvn_tex* t, const int crop[4]) { log_frame_stats(); } if (dev.offscreen || interpolated) return; - present_image(src, sw, sh, crop); + present_image(src, sw, sh, crop, direct_view); } } // namespace diff --git a/readme.md b/readme.md index 5db1b09c..e250687e 100644 --- a/readme.md +++ b/readme.md @@ -208,6 +208,7 @@ and changes, the bottom face button changes too, and the right one closes the me | FPS counter | Off, On | With Game speed on Per frame, the game runs slower whenever it is below 60 fps. Real time keeps it at normal speed. +Upscaling and Sharpness also work when Frame generation is Off. ### Controls diff --git a/tests/test_upscale_independent.py b/tests/test_upscale_independent.py new file mode 100644 index 00000000..9601d548 --- /dev/null +++ b/tests/test_upscale_independent.py @@ -0,0 +1,65 @@ +"""Execute production scaler/init functions with authored API spies, without a game or Vulkan device. + +The direct-present view's forwarding/age bookkeeping is a separate, explicitly source-based wiring check. +--scale-source can supply an original vk_framegen.cpp for the runtime negative control. +""" +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + +ROOT = Path(__file__).resolve().parents[1] + + +def block(source, marker): + if source.count(marker) != 1: + raise ValueError("Expected exactly one " + marker) + start = source.index(marker) + end = source.index("{", start) + 1 + depth = 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end] + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--cxx", default="g++") + p.add_argument("--scale-source", type=Path) + a = p.parse_args() + source = (ROOT / "gpu/vk/vk_framegen.cpp").read_text(encoding="utf-8") + scale_source = a.scale_source.read_text(encoding="utf-8") if a.scale_source else source + replay = block((ROOT / "gpu/vk/vk_replay.cpp").read_text(encoding="utf-8"), "void do_present(") + # Wiring check only: resource/dispatch behavior is exercised by the real functions below. + capture = replay.index("direct_view = texture_view(") + age = replay.index("texture_frame(completed());") + assert replay.index("std::lock_guard lk(replay_mu)") < capture < age + assert capture < replay.index("tex->last_frame = texture_frame_no();") < age + assert capture < replay.index("tex->last_value = std::max(tex->last_value, value);") < age + assert "present_image(src, sw, sh, crop, direct_view);" in replay + functions = (block(source, "struct ScaleState") + " scale;\n" + block(source, "bool init_scale(") + "\n" + + block(source, "bool framegen_wanted(") + "\n" + block(source, "bool framegen_on(") + "\n" + + block(scale_source, "VkImage framegen_scale(")) + template = (ROOT / "tests/upscale_independent_test.cpp").read_text(encoding="utf-8") + assert template.count("// PRODUCTION_FUNCTIONS") == 1 + fixture = template.replace("// PRODUCTION_FUNCTIONS", functions) + with tempfile.TemporaryDirectory(prefix="upscale-independent-") as tmp: + owned = Path(tmp).resolve() + assert owned.parent == Path(tempfile.gettempdir()).resolve() and owned.name.startswith("upscale-independent-") + cpp, exe = Path(tmp) / "fixture.cpp", Path(tmp) / "fixture.exe" + cpp.write_text(fixture, encoding="utf-8") + subprocess.run([a.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(cpp), "-o", str(exe)], check=True) + for allow in ("0", "1"): + env = os.environ.copy() + env["RX_VK_FRAMEGEN"] = allow + if allow == "0": + env["RX_VK_UPSCALE_LOG"] = "1" + else: + env.pop("RX_VK_UPSCALE_LOG", None) + subprocess.run([str(exe)], env=env, check=True) + + +if __name__ == "__main__": + main() diff --git a/tests/upscale_independent_test.cpp b/tests/upscale_independent_test.cpp new file mode 100644 index 00000000..a6900ebb --- /dev/null +++ b/tests/upscale_independent_test.cpp @@ -0,0 +1,156 @@ +// Authored API spies around functions inserted from vk_framegen.cpp by test_upscale_independent.py. +#ifdef _WIN32 +#define _CRT_SECURE_NO_WARNINGS +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#define CHECK(x) do { if (!(x)) { std::fprintf(stderr, "FAIL:%d %s\n", __LINE__, #x); std::exit(1); } } while (0) +using VkImage = uint64_t; +using VkImageView = uint64_t; +using VkSampler = uint64_t; +using VkCommandBuffer = int; +using VkDescriptorType = int; +using VkImageUsageFlags = int; +constexpr uint64_t VK_NULL_HANDLE = 0; +constexpr int VK_SUCCESS = 0, VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO = 1; +constexpr int VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3; +constexpr int VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE = 4, VK_FILTER_LINEAR = 5, VK_FILTER_NEAREST = 6; +constexpr int VK_FORMAT_R8G8B8A8_UNORM = 7, VK_IMAGE_LAYOUT_GENERAL = 8, VK_IMAGE_ASPECT_COLOR_BIT = 9; +constexpr int VK_IMAGE_USAGE_SAMPLED_BIT = 1, VK_IMAGE_USAGE_TRANSFER_DST_BIT = 2; +constexpr int VK_IMAGE_USAGE_STORAGE_BIT = 4, VK_IMAGE_USAGE_TRANSFER_SRC_BIT = 8; +const uint32_t FG_SCALE_COMP_SPV[] = {123}; +struct Img { VkImage image = 0; VkImageView view = 0; }; +struct Pipe { uint64_t set = 0, layout = 0, pipe = 0; }; +struct VkSamplerCreateInfo { + int sType, addressModeU = 0, addressModeV = 0, addressModeW = 0; + float maxLod = 0; int magFilter = 0, minFilter = 0; +}; +struct Offset { int x = 0, y = 0, z = 0; }; +struct Layers { int aspect = 0, level = 0, first = 0, count = 0; }; +struct VkImageBlit { Layers srcSubresource, dstSubresource; Offset srcOffsets[2], dstOffsets[2]; }; +struct { int upscale = 0, sharpness = 0, framegen = 0; } opts; +struct SpyMutex { + bool held = false; + void lock() { CHECK(!held); held = true; } + void unlock() { CHECK(held); held = false; } +}; +struct { bool push_descriptor = true, offscreen = false; int device = 1; SpyMutex present_mu; } dev; +// Only used by framegen_on and by the original scaler's negative control. No interpolation is initialized. +struct { + bool init = false, failed = false, thread_started = false; + Pipe scale; VkSampler linear = 0; Img cropped, scaled; + uint32_t cropped_w = 0, cropped_h = 0, scaled_w = 0, scaled_h = 0; +} fg; +bool vkCmdPushDescriptorSetKHR = true; +int pipe_calls = 0, sampler_calls = 0, image_calls = 0, dispatches = 0, blits = 0, logs = 0; +int destroyed_pipes = 0, destroyed_layouts = 0, destroyed_sets = 0, waits = 0, flushes = 0; +bool pipe_fail = false, sampler_fail = false, image_fail = false; +uint64_t next_handle = 100; +uint32_t made_w = 0, made_h = 0, dispatched_w = 0, dispatched_h = 0; +int made_usage = 0, dispatched_mode = -1; +float dispatched_sharp = -1; +VkImageView sampled_view = 0; +VkImageBlit last_blit; +bool make_pipe(Pipe& p, const uint32_t* code, size_t bytes, std::initializer_list types, uint32_t push) { + CHECK(code == FG_SCALE_COMP_SPV && bytes == sizeof FG_SCALE_COMP_SPV && types.size() == 2 && push == 8); + CHECK(*types.begin() == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER && *(types.begin() + 1) == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); + ++pipe_calls; p.set = 1; p.layout = 2; p.pipe = pipe_fail ? 0 : 3; return !pipe_fail; +} +int vkCreateSampler(int, const VkSamplerCreateInfo* s, void*, VkSampler* out) { + CHECK(s->magFilter == VK_FILTER_LINEAR && s->minFilter == VK_FILTER_LINEAR); + ++sampler_calls; *out = sampler_fail ? 0 : 4; return sampler_fail ? -1 : VK_SUCCESS; +} +void vkDestroyPipeline(int, uint64_t, void*) { ++destroyed_pipes; } +void vkDestroyPipelineLayout(int, uint64_t, void*) { ++destroyed_layouts; } +void vkDestroyDescriptorSetLayout(int, uint64_t, void*) { ++destroyed_sets; } +void queue_flush_locked() { CHECK(dev.present_mu.held); ++flushes; } +void vkDeviceWaitIdle(int) { + CHECK(dev.present_mu.held); // the real scaler must take the submit lock before waiting + CHECK(flushes > waits); // upstream batching must be drained before waiting for idle + ++waits; +} +void drop_img(Img& i) { i = {}; } +bool make_img(Img& i, uint32_t w, uint32_t h, int format, int usage) { + CHECK(w && h && format == VK_FORMAT_R8G8B8A8_UNORM); + ++image_calls; made_w = w; made_h = h; made_usage = usage; + if (image_fail) { image_fail = false; i.image = ++next_handle; return false; } // image made, view failed + i.image = ++next_handle; i.view = ++next_handle; return true; +} +void to_general(int, VkImage image) { CHECK(image); } +void full_barrier(int) {} +void vkCmdBlitImage(int, VkImage src, int, VkImage dst, int, int count, const VkImageBlit* b, int filter) { + CHECK(src == 11 && dst && count == 1 && filter == VK_FILTER_NEAREST); + ++blits; last_blit = *b; +} +void pass(int, const Pipe& p, std::initializer_list> b, + uint32_t w, uint32_t h, const void* data, uint32_t bytes) { + CHECK(p.pipe == 3 && b.size() == 2 && b.begin()->first && b.begin()->second == 4 && bytes == 8); + CHECK((b.begin() + 1)->first && !(b.begin() + 1)->second); + struct PC { int mode; float sharp; } pc; + std::memcpy(&pc, data, sizeof pc); + ++dispatches; dispatched_mode = pc.mode; dispatched_sharp = pc.sharp; + dispatched_w = w; dispatched_h = h; sampled_view = b.begin()->first; +} +void logf(const char*, ...) { CHECK(dispatches > 0); ++logs; } +// PRODUCTION_FUNCTIONS +void reset() { + scale = {}; fg = {}; opts = {}; dev.push_descriptor = true; vkCmdPushDescriptorSetKHR = true; + pipe_calls = sampler_calls = image_calls = dispatches = blits = waits = flushes = 0; + destroyed_pipes = destroyed_layouts = destroyed_sets = 0; + pipe_fail = sampler_fail = image_fail = false; +} +VkImage draw(const int* crop = nullptr, uint32_t dw = 2560, uint32_t dh = 1440) { + return framegen_scale(1, 12, 11, 2560, 1440, crop, dw, dh); +} +int main() { + reset(); CHECK(!draw()); CHECK(pipe_calls == 0 && image_calls == 0 && dispatches == 0); + opts.upscale = 1; + CHECK(!framegen_scale(1, 0, 11, 2560, 1440, nullptr, 100, 100)); + CHECK(!framegen_scale(1, 12, 11, 0, 1440, nullptr, 100, 100)); + CHECK(!draw(nullptr, 0)); + const int bad[][4] = {{-1,0,10,10}, {2560,0,10,10}, {0,1440,10,10}}; + for (const auto& crop : bad) CHECK(!draw(crop)); + CHECK(pipe_calls == 0 && image_calls == 0); + for (int mode : {1, 2, 0}) { + reset(); opts.upscale = mode; opts.sharpness = mode ? 0 : 37; + CHECK(!framegen_on() && !fg.init && !fg.thread_started); + CHECK(draw()); // original FG-off implementation returns null here + CHECK(pipe_calls == 1 && sampler_calls == 1 && dispatches == 1 && image_calls == 1); + CHECK(dispatched_mode == mode && dispatched_sharp == (mode ? 0.0f : 0.37f)); + CHECK(dispatched_w == 2560 && dispatched_h == 1440 && sampled_view == 12); + CHECK(made_usage == (VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT)); + CHECK(draw()); CHECK(pipe_calls == 1 && sampler_calls == 1 && image_calls == 1); + CHECK(!fg.init && !fg.thread_started && !framegen_on()); + opts.upscale = opts.sharpness = 0; CHECK(!draw()); + CHECK(dispatches == 2 && image_calls == 1 && pipe_calls == 1); + } + reset(); opts.upscale = 1; opts.framegen = std::atoi(std::getenv("RX_VK_FRAMEGEN")) == 0 ? 2 : 0; + CHECK(!framegen_on()); CHECK(draw()); CHECK(!fg.init && !fg.thread_started); + reset(); opts.upscale = 1; pipe_fail = true; + CHECK(!draw() && scale.failed && !fg.failed); CHECK(pipe_calls == 1 && sampler_calls == 0 && dispatches == 0); + CHECK(destroyed_sets == 1 && destroyed_layouts == 1 && destroyed_pipes == 0); + pipe_fail = false; CHECK(!draw() && pipe_calls == 1); // cached failure, no per-frame allocation loop + reset(); opts.upscale = 2; sampler_fail = true; + CHECK(!draw() && dispatches == 0 && image_calls == 0); + CHECK(destroyed_sets == 1 && destroyed_layouts == 1 && destroyed_pipes == 1); + reset(); opts.upscale = 1; dev.push_descriptor = false; CHECK(!draw()); CHECK(pipe_calls == 0); + reset(); opts.upscale = 1; CHECK(draw()); + image_fail = true; CHECK(!draw(nullptr, 1920, 1080)); + CHECK(scale.scaled_w == 0 && scale.scaled_h == 0 && !scale.scaled.image); + CHECK(draw()); CHECK(image_calls == 3 && dispatches == 2); // old size must allocate again after failure + reset(); opts.upscale = 2; + const int crop[] = {2500, 1400, 100, 100}; CHECK(draw(crop, 1920, 1080)); + CHECK(blits == 1 && last_blit.srcOffsets[1].x == 2560 && last_blit.srcOffsets[1].y == 1440); + CHECK(last_blit.dstOffsets[1].x == 60 && last_blit.dstOffsets[1].y == 40 && sampled_view == scale.cropped.view); + const int resized_crop[] = {0,0,1280,720}; image_fail = true; CHECK(!draw(resized_crop)); + CHECK(!scale.cropped.image && scale.cropped_w == 0 && scale.cropped_h == 0); + CHECK(draw(crop, 1920, 1080)); CHECK(blits == 2 && !fg.init && !fg.thread_started); + CHECK(logs == (std::getenv("RX_VK_UPSCALE_LOG") ? 1 : 0)); + std::puts("PASS: production scaler works independently, caches/retries safely, and leaves interpolation uninitialized"); +}