Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/source-regressions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
89 changes: 67 additions & 22 deletions gpu/vk/vk_framegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
9 changes: 8 additions & 1 deletion gpu/vk/vk_replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 65 additions & 0 deletions tests/test_upscale_independent.py
Original file line number Diff line number Diff line change
@@ -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()
Loading
Loading