From d5af61d4a472da8e7a7450951cf9d13651272b4d Mon Sep 17 00:00:00 2001 From: klimovich008 Date: Sun, 8 Nov 2026 01:41:20 +0100 Subject: [PATCH] Decode RGB9E5 inputs in the failed upscale fallback --- gpu/vk/vk_draw.cpp | 99 +++++++++---- tests/test_upscale_fallback.py | 253 +++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+), 25 deletions(-) create mode 100644 tests/test_upscale_fallback.py diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 0aeaed65c..494d6372f 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -4472,6 +4472,9 @@ static bool movie_keep_all_networks() { return keep; } +static bool typed_upscale(VkCommandBuffer cmd, VkImageView input_view, VkImageView output_view, + Texture* out, uint32_t level, uint64_t key, const char* keys, bool temporal); + bool upscale_stub(VkCommandBuffer cmd, const NvnState& s, uint64_t key) { static const bool off = getenv("RX_VK_NO_UPSCALE_STUB") != nullptr; if (off) return false; @@ -4531,6 +4534,37 @@ bool upscale_stub(VkCommandBuffer cmd, const NvnState& s, uint64_t key) { logf("vk: movie upscaling uses the linear compute path"); return true; } + // The generic failed-network path can receive RGB9E5 bits in a shared R11 image. + // Match the observed base-level 2D case only; other fallbacks keep their existing blit. + if (!gameplay_network(key) && in.d.format == NVN_FORMAT_RGB9E5F && out.d.format == NVN_FORMAT_R11G11B10F && + src->format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && dst->format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && + (fd.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && + src->type == VK_IMAGE_TYPE_2D && dst->type == VK_IMAGE_TYPE_2D && src->depth == 1 && dst->depth == 1 && + src->layers == 1 && dst->layers == 1 && src->levels == 1 && dst->levels == 1 && !src->skip && !dst->skip && + in.d.swizzle_r == NVN_TEXTURE_SWIZZLE_R && in.d.swizzle_g == NVN_TEXTURE_SWIZZLE_G && + in.d.swizzle_b == NVN_TEXTURE_SWIZZLE_B) { + VkFormatProperties e5{}; + vkGetPhysicalDeviceFormatProperties(dev.physical, VK_FORMAT_E5B9G9R9_UFLOAT_PACK32, &e5); + const VkFormatFeatureFlags sampled = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + nvn_tex selected_in{}, selected_out{}; + nvn_view iv{}, ov{}; + bool ihv = false, ohv = false; + if ((e5.optimalTilingFeatures & sampled) == sampled && + texture_of((int)(in_h & 0xFFFFF), &selected_in, &iv, &ihv) && + texture_of((int)(out_h & 0xFFFFF), &selected_out, &ov, &ohv) && !ihv && !ohv && + selected_in.serial == in.serial && selected_out.serial == out.serial) { + VkFormat input_format = VK_FORMAT_UNDEFINED, output_format = VK_FORMAT_UNDEFINED; + const VkImageView input_view = texture_view(src, &in, nullptr, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Sample, &input_format); + const VkImageView output_view = texture_view(dst, &out, nullptr, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Storage, + &output_format, VK_FORMAT_B10G11R11_UFLOAT_PACK32); + if (input_format == VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 && output_format == VK_FORMAT_B10G11R11_UFLOAT_PACK32 && + typed_upscale(cmd, input_view, output_view, dst, 0, key, "generic RGB9E5 fallback", false)) { + full_barrier(cmd); + texture_written(dst); + return true; + } + } + } VkImageBlit bl{}; bl.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; bl.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; @@ -4575,21 +4609,9 @@ bool taau_key(uint64_t key) { } return match; } -bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { - const char* keys = getenv("RX_VK_TAAU_KEYS") ? getenv("RX_VK_TAAU_KEYS") : "fd029a317a9a4b94"; - if (!taau_key(p.key) || !dev.push_descriptor || !vkCmdPushDescriptorSetKHR || p.stages.empty()) return false; - const Stage& st = p.stages[0]; - if (st.info.texture_descriptors.empty() || st.info.image_descriptors.empty()) return false; - size_t ntex = 0; // (build_descriptors' image infos: the textures', then the images') - for (const auto& d : st.info.texture_descriptors) ntex += d.count; - if (dw.imgs.size() <= ntex || !dw.imgs[0].imageView || !dw.imgs[ntex].imageView) return false; - const auto& od = st.info.image_descriptors[0]; - Texture* out = nullptr; - uint32_t level = 0; - const uint32_t handle = handle_of(s, st, od, 0); - if (!handle) return false; - texture_view_of(handle, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Storage, image_format(od.format), &out, &level); - if (!out || out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32) return false; +static bool typed_upscale(VkCommandBuffer cmd, VkImageView input_view, VkImageView output_view, + Texture* out, uint32_t level, uint64_t key, const char* keys, bool temporal) { + if (!dev.push_descriptor || !vkCmdPushDescriptorSetKHR || !input_view || !output_view || !out) return false; static VkPipeline pipe = VK_NULL_HANDLE; static VkPipelineLayout layout = VK_NULL_HANDLE; static VkSampler linear = VK_NULL_HANDLE; @@ -4622,18 +4644,28 @@ bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; si.maxLod = 0.25f; vkCreateSampler(dev.device, &si, nullptr, &linear); - logf("vk: the TAAU network %s is stood in for (an upscale of its input; RX_VK_TAAU_KEYS=- keeps it)%s", keys, + logf("vk: typed upscale helper %s%s", keys, pipe ? "" : ": its pipeline was not made"); } if (!pipe) return false; uint32_t size[3] = {std::max(1u, out->width >> level), std::max(1u, out->height >> level), 0}; // (the stand-in's last output, which bounds this one's pixels: shaders/taau_upscale.comp. Made again at a new size, // the first frame unbounded; RX_VK_TAAU_NO_CLAMP=1: never bounded) - static VkImage hist = VK_NULL_HANDLE; - static VmaAllocation hist_mem = nullptr; - static VkImageView hist_view = VK_NULL_HANDLE; - static uint32_t hist_w = 0, hist_h = 0; - static uint64_t hist_frame = 0; + struct History { + VkImage image = VK_NULL_HANDLE; + VmaAllocation memory = nullptr; + VkImageView view = VK_NULL_HANDLE; + uint32_t width = 0, height = 0; + uint64_t frame = 0; + }; + static History histories[2]; + History& history = histories[temporal ? 0 : 1]; + auto& hist = history.image; + auto& hist_mem = history.memory; + auto& hist_view = history.view; + auto& hist_w = history.width; + auto& hist_h = history.height; + auto& hist_frame = history.frame; // (off by default: bounding a pixel by the last frame's smeared everything that moves (the user: blurry, warping) // and only dimmed the flashes; RX_VK_TAAU_CLAMP=1 turns it on) static const bool no_clamp = getenv("RX_VK_TAAU_CLAMP") == nullptr; @@ -4663,10 +4695,10 @@ bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { } // (bounded when the history is last frame's or so: a pause of the TAAU, a menu, starts again unbounded) const uint64_t now_frame = texture_frame_no(); - size[2] = !no_clamp && hist_frame && now_frame - hist_frame <= 3 ? 1u : 0u; + size[2] = temporal && !no_clamp && hist_frame && now_frame - hist_frame <= 3 ? 1u : 0u; hist_frame = now_frame; - const VkDescriptorImageInfo in_info{linear, dw.imgs[0].imageView, VK_IMAGE_LAYOUT_GENERAL}; - const VkDescriptorImageInfo out_info{VK_NULL_HANDLE, dw.imgs[ntex].imageView, VK_IMAGE_LAYOUT_GENERAL}; + const VkDescriptorImageInfo in_info{linear, input_view, VK_IMAGE_LAYOUT_GENERAL}; + const VkDescriptorImageInfo out_info{VK_NULL_HANDLE, output_view, VK_IMAGE_LAYOUT_GENERAL}; const VkDescriptorImageInfo hist_info{VK_NULL_HANDLE, hist_view, VK_IMAGE_LAYOUT_GENERAL}; VkWriteDescriptorSet w[3]{{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}, {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}}; @@ -4681,9 +4713,26 @@ bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, 12, size); vkCmdDispatch(cmd, (size[0] + 7) / 8, (size[1] + 7) / 8, 1); descriptors_disturbed(); // (the program's own pushes are gone from the bind point) - probe_image(cmd, out, level, 0, p.key, 't'); + if (temporal) probe_image(cmd, out, level, 0, key, 't'); return true; } +bool taau_stand_in(VkCommandBuffer cmd, const NvnState& s, const Program& p) { + const char* keys = getenv("RX_VK_TAAU_KEYS") ? getenv("RX_VK_TAAU_KEYS") : "fd029a317a9a4b94"; + if (!taau_key(p.key) || !dev.push_descriptor || !vkCmdPushDescriptorSetKHR || p.stages.empty()) return false; + const Stage& st = p.stages[0]; + if (st.info.texture_descriptors.empty() || st.info.image_descriptors.empty()) return false; + size_t ntex = 0; // (build_descriptors' image infos: the textures', then the images') + for (const auto& d : st.info.texture_descriptors) ntex += d.count; + if (dw.imgs.size() <= ntex || !dw.imgs[0].imageView || !dw.imgs[ntex].imageView) return false; + const auto& od = st.info.image_descriptors[0]; + Texture* out = nullptr; + uint32_t level = 0; + const uint32_t handle = handle_of(s, st, od, 0); + if (!handle) return false; + texture_view_of(handle, VK_IMAGE_VIEW_TYPE_2D, ViewUse::Storage, image_format(od.format), &out, &level); + if (!out || out->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32) return false; + return typed_upscale(cmd, dw.imgs[0].imageView, dw.imgs[ntex].imageView, out, level, p.key, keys, true); +} bool precompile_wanted() { #if defined(HOOKSHOT_RELEASE) diff --git a/tests/test_upscale_fallback.py b/tests/test_upscale_fallback.py new file mode 100644 index 000000000..6a590f852 --- /dev/null +++ b/tests/test_upscale_fallback.py @@ -0,0 +1,253 @@ +"""Exercise the actual generic upscale fallback without Vulkan or a GPU. + +python tests/test_upscale_fallback.py [--source gpu/vk/vk_draw.cpp] [--cxx clang++] +Use --cxx g++ on Linux, or a configured Windows compiler shell. No SDK needed. +""" +import argparse +import os +from pathlib import Path +import subprocess +import tempfile + +ROOT = next(p for p in Path(__file__).resolve().parents if (p / 'gpu/vk/vk_draw.cpp').is_file()) + +def block(text, marker): + start = text.index(marker) + end = text.index('{', start) + 1 + depth = 1 + while depth: + depth += (text[end] == '{') - (text[end] == '}') + end += 1 + return text[start:end] + +PREFIX = r''' +#define _CRT_SECURE_NO_WARNINGS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Only the Vulkan declarations used by upscale_stub; no Vulkan SDK or runtime. +using VkCommandBuffer = uintptr_t; +using VkImage = uintptr_t; +using VkImageView = uintptr_t; +using VkPhysicalDevice = uintptr_t; +using VkFormat = uint32_t; +using VkFormatFeatureFlags = uint32_t; +using VkImageAspectFlags = uint32_t; +using VkImageType = uint32_t; +using VkImageViewType = uint32_t; +constexpr uint32_t VK_IMAGE_VIEW_TYPE_2D = 1; +using VkSampleCountFlagBits = uint32_t; +using VkImageLayout = uint32_t; +using VkFilter = uint32_t; +constexpr uint32_t VK_FORMAT_UNDEFINED = 0, VK_FORMAT_B10G11R11_UFLOAT_PACK32 = 122, + VK_FORMAT_E5B9G9R9_UFLOAT_PACK32 = 123, VK_FORMAT_R8G8B8A8_UNORM = 37; +constexpr uint32_t VK_IMAGE_TYPE_2D = 1, VK_IMAGE_ASPECT_COLOR_BIT = 1, VK_SAMPLE_COUNT_1_BIT = 1, + VK_IMAGE_LAYOUT_GENERAL = 1, VK_FILTER_LINEAR = 1; +constexpr uint32_t VK_FORMAT_FEATURE_BLIT_SRC_BIT = 1, VK_FORMAT_FEATURE_BLIT_DST_BIT = 2, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT = 4, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT = 8, VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT = 16; +struct VkFormatProperties { + VkFormatFeatureFlags optimalTilingFeatures{}; +}; +struct Subresource { + uint32_t aspectMask{}, mipLevel{}, baseArrayLayer{}, layerCount{}; +}; +struct Offset { + int32_t x{}, y{}, z{}; +}; +struct VkImageBlit { + Subresource srcSubresource; + Offset srcOffsets[2]; + Subresource dstSubresource; + Offset dstOffsets[2]; +}; +constexpr int NVN_FORMAT_RGB9E5F = 64, NVN_FORMAT_R11G11B10F = 63; +constexpr unsigned NVN_TEXTURE_SWIZZLE_R = 2, NVN_TEXTURE_SWIZZLE_G = 3, NVN_TEXTURE_SWIZZLE_B = 4; +struct Desc { + int target = 2, format = 64; + unsigned width = 64, height = 32, depth = 1; + unsigned swizzle_r = 2, swizzle_g = 3, swizzle_b = 4; +}; +struct nvn_tex { + Desc d; + uint64_t gpu = 10, serial = 99; +}; +struct nvn_view {}; +struct NvnState { + uint64_t tex[6][128]{}, img[6][32]{}; +}; +struct Texture { + VkImage image = (VkImage)11; + VkFormat format = VK_FORMAT_B10G11R11_UFLOAT_PACK32; + unsigned width = 64, height = 32, depth = 1, layers = 1, levels = 1, skip = 0; + VkImageType type = VK_IMAGE_TYPE_2D; + VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT; + bool bc = false; + VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT; +}; +Texture src, dst; +nvn_tex source, destination; +bool override_view = false, movie = false, pipeline_ok = true; +bool missing_storage = false, missing_sample = false, missing_linear = false; +unsigned blits = 0, typed_calls = 0, views = 0, movie_calls = 0, writes = 0; +struct { + VkPhysicalDevice physical{}; +} dev; +void cs_slot_counts(const NvnState &, uint32_t *t, uint32_t *i) { *t = *i = 1; } +bool texture_of(int h, nvn_tex *t, nvn_view *v, bool *hv) { + assert(h == 1 || h == 2); + *t = h == 1 ? source : destination; + *v = {}; + *hv = override_view; + return true; +} +Texture *texture_get(const nvn_tex *t, bool) { return t->gpu == 10 ? &src : &dst; } +void vkGetPhysicalDeviceFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties *p) { + p->optimalTilingFeatures = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; + if (missing_storage && format == VK_FORMAT_B10G11R11_UFLOAT_PACK32) + p->optimalTilingFeatures &= ~VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; + if (missing_sample && format == VK_FORMAT_E5B9G9R9_UFLOAT_PACK32) + p->optimalTilingFeatures &= ~VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT; + if (missing_linear && format == VK_FORMAT_E5B9G9R9_UFLOAT_PACK32) + p->optimalTilingFeatures &= ~VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; +} +void end_pass(VkCommandBuffer) {} +void full_barrier(VkCommandBuffer) {} +bool dump_on() { return false; } +void logf(const char *, ...) {} +void texture_written(Texture *t) { + assert(t == &dst); + writes++; +} +void dump_image(VkCommandBuffer, Texture *, unsigned, unsigned, const char *) {} +bool movie_upscale(VkCommandBuffer, uint64_t, uint64_t, Texture *) { + movie_calls++; + return movie; +} +bool gameplay_network(uint64_t key) { + return key == 0xfd029a317a9a4b94ull || key == 0x9dc98871eb7fc101ull; +} +enum class ViewUse { Sample, Storage }; +VkImageView texture_view(Texture *t, const nvn_tex *, const nvn_view *v, VkImageViewType type, + ViewUse use, VkFormat *f, VkFormat forced = VK_FORMAT_UNDEFINED) { + assert(!v && type == VK_IMAGE_VIEW_TYPE_2D); + views++; + if (t == &src) { + assert(use == ViewUse::Sample && forced == VK_FORMAT_UNDEFINED); + *f = VK_FORMAT_E5B9G9R9_UFLOAT_PACK32; + return (VkImageView)101; + } + assert(t == &dst && use == ViewUse::Storage && forced == VK_FORMAT_B10G11R11_UFLOAT_PACK32); + *f = VK_FORMAT_B10G11R11_UFLOAT_PACK32; + return (VkImageView)102; +} +static bool typed_upscale(VkCommandBuffer, VkImageView input, VkImageView output, Texture *t, + uint32_t level, uint64_t, const char *, bool temporal) { + assert(input == (VkImageView)101 && output == (VkImageView)102 && t == &dst && level == 0 && + !temporal); + typed_calls++; + return pipeline_ok; +} +void vkCmdBlitImage(VkCommandBuffer, VkImage a, VkImageLayout al, VkImage b, VkImageLayout bl, + uint32_t n, const VkImageBlit *r, VkFilter filter) { + assert(a == src.image && b == dst.image && al == VK_IMAGE_LAYOUT_GENERAL && + bl == VK_IMAGE_LAYOUT_GENERAL && n == 1 && filter == VK_FILTER_LINEAR); + assert(r->srcSubresource.mipLevel == 0 && r->srcSubresource.baseArrayLayer == 0 && + r->dstSubresource.mipLevel == 0 && r->dstSubresource.baseArrayLayer == 0); + assert(r->srcOffsets[1].x == 64 && r->srcOffsets[1].y == 32 && r->dstOffsets[1].x == 128 && + r->dstOffsets[1].y == 64); + blits++; +} +''' +SUFFIX = r''' +int main(int argc, char **argv) { + assert(argc == 2); + const std::string mode = argv[1]; + uint64_t key = 0x0775215f8da720e6ull; + destination.gpu = 20; + destination.d.format = 63; + destination.d.width = 128; + destination.d.height = 64; + dst.width = 128; + dst.height = 64; + dst.image = (VkImage)22; + if (mode == "r11") + source.d.format = 63; + if (mode == "movie") { + dst.format = VK_FORMAT_R8G8B8A8_UNORM; + movie = true; + } + if (mode == "view") + override_view = true; + if (mode == "skip") + src.skip = 1; + if (mode == "mip") + src.levels = 2; + if (mode == "swizzle") + source.d.swizzle_r = 4; + if (mode == "fd") + key = 0xfd029a317a9a4b94ull; + if (mode == "9dc") + key = 0x9dc98871eb7fc101ull; + if (mode == "pipeline-failure") + pipeline_ok = false; + if (mode == "no-storage") + missing_storage = true; + if (mode == "no-e5-sample") + missing_sample = true; + if (mode == "no-e5-linear") + missing_linear = true; + NvnState state; + state.tex[5][0] = 1; + state.img[5][0] = 2; + assert(upscale_stub((VkCommandBuffer)5, state, key)); + if (mode == "captured") { + if (typed_calls != 1 || views != 2 || blits != 0) { + puts("RED: captured RGB9E5 input still used physical-format blit"); + return 7; + } + assert(writes == 1); + } else if (mode == "movie") { + assert(movie_calls == 1 && typed_calls == 0 && blits == 0 && writes == 1); + } else if (mode == "pipeline-failure") { + assert(typed_calls == 1 && views == 2 && blits == 1 && writes == 1); + } else { + assert(typed_calls == 0 && views == 0 && blits == 1 && writes == 1); + } + printf("PASS: %s\n", mode.c_str()); +} +''' + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=ROOT / 'gpu/vk/vk_draw.cpp') + parser.add_argument('--cxx', default='clang++') + args = parser.parse_args() + body = block(args.source.read_text(encoding='utf-8'), 'bool upscale_stub(') + env = os.environ.copy() + env.pop('RX_VK_NO_UPSCALE_STUB', None) + with tempfile.TemporaryDirectory(prefix='upscale-fallback-') as tmp: + tmp = Path(tmp) + source = tmp / 'check.cpp' + exe = tmp / ('check.exe' if os.name == 'nt' else 'check') + source.write_text(PREFIX + body + SUFFIX, encoding='utf-8') + subprocess.run([args.cxx, '-std=c++20', '-UNDEBUG', str(source), '-o', str(exe)], env=env, check=True) + cases = ('captured', 'r11', 'movie', 'view', 'skip', 'mip', 'swizzle', 'fd', '9dc', + 'pipeline-failure', 'no-storage', 'no-e5-sample', 'no-e5-linear') + for case in cases: + subprocess.run([str(exe), case], env=env, check=True) + +if __name__ == '__main__': + main()