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
34 changes: 34 additions & 0 deletions .github/workflows/source-regressions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
- nx2/unpack.py
- nx2/test_nup_paths.py
- tests/test_make_game_cache.py
- gpu/vk/vk_present_rect.h
- gpu/vk/tests/present_rect_test.cpp
- gpu/vk/vk_replay.cpp
- tests/test_child_unicode.py
- gpu/vk/vk_replay.cpp
- gpu/vk/vk_state.h
Expand Down Expand Up @@ -67,6 +70,9 @@ on:
- nx2/unpack.py
- nx2/test_nup_paths.py
- tests/test_make_game_cache.py
- gpu/vk/vk_present_rect.h
- gpu/vk/tests/present_rect_test.cpp
- gpu/vk/vk_replay.cpp
- tests/test_child_unicode.py
- gpu/vk/vk_replay.cpp
- gpu/vk/vk_state.h
Expand Down Expand Up @@ -148,6 +154,34 @@ jobs:
run: python -I -B nx2/test_nup_paths.py
- name: Check source-cache coherency
run: python -I -B tests/test_make_game_cache.py
- name: Check Vulkan presentation rectangle
if: runner.os == 'Windows'
shell: cmd
run: |
@echo off
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
if not exist "%VSWHERE%" (
echo Visual Studio locator not found.
exit /b 1
)
set "VSROOT="
for /f "usebackq delims=" %%i in (`call "%VSWHERE%" -latest -products * -version "[17.0,18.0)" -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSROOT=%%i"
if not defined VSROOT (
echo Visual Studio 2022 C++ tools not found.
exit /b 1
)
if not exist "%VSROOT%\Common7\Tools\VsDevCmd.bat" (
echo Visual Studio developer command prompt not found.
exit /b 1
)
call "%VSROOT%\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64
if errorlevel 1 exit /b 1
where cl
if errorlevel 1 exit /b 1
cl /nologo /std:c++17 /W4 /WX /EHsc /Fo"%TEMP%\present_rect_test.obj" /Fe:"%TEMP%\present_rect_test.exe" gpu\vk\tests\present_rect_test.cpp
if errorlevel 1 exit /b 1
"%TEMP%\present_rect_test.exe"
if errorlevel 1 exit /b 1
- name: Check isolated child Unicode output
run: python -I -B tests/test_child_unicode.py
- name: Check a failed step's error line
Expand Down
40 changes: 40 additions & 0 deletions gpu/vk/tests/present_rect_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Game-free regression for the production Vulkan presentation rectangle helper.
#include <cstdio>

#include "../vk_present_rect.h"

namespace {
int failures = 0;

void expect(const char* name, mkwvk::PresentRect got, mkwvk::PresentRect want) {
if (got.x == want.x && got.y == want.y && got.w == want.w && got.h == want.h) return;
std::fprintf(stderr, "%s: got {%u,%u,%u,%u}, wanted {%u,%u,%u,%u}\n", name, got.x, got.y, got.w, got.h,
want.x, want.y, want.w, want.h);
failures++;
}

} // namespace

int main() {
using mkwvk::PresentRect;
const PresentRect fit_16_10 = mkwvk::present_rect(1920, 1080, nullptr, 1600, 1000);
expect("16:9 into 16:10", fit_16_10, {0, 50, 1600, 900});
expect("16:9 same aspect", mkwvk::present_rect(1920, 1080, nullptr, 1920, 1080), {0, 0, 1920, 1080});
expect("explicit ultrawide camera mode retains fill",
mkwvk::present_rect(1920, 1080, nullptr, 3440, 1440, true), {0, 0, 3440, 1440});
expect("ultrawide source aspect fits naturally",
mkwvk::present_rect(3440, 1440, nullptr, 3440, 1440), {0, 0, 3440, 1440});
expect("ultrawide source remains wider than 16:9",
mkwvk::present_rect(3440, 1440, nullptr, 1600, 1000), {0, 165, 1600, 669});
expect("portrait target", mkwvk::present_rect(1920, 1080, nullptr, 1000, 1600), {0, 519, 1000, 562});
expect("odd resized destination", mkwvk::present_rect(1920, 1080, nullptr, 1601, 1001), {0, 50, 1601, 900});

const int crop_16_9[4] = {160, 90, 2240, 1260};
expect("cropped 16:9 content", mkwvk::present_rect(2560, 1440, crop_16_9, 1600, 1000), {0, 50, 1600, 900});
const int crop_odd[4] = {7, 11, 1900, 1069};
expect("odd crop and resized target", mkwvk::present_rect(1920, 1080, crop_odd, 1600, 1000), {0, 50, 1600, 900});
const int crop_at_edge[4] = {90, 70, 20, 20};
expect("small crop clamps to source edge", mkwvk::present_rect(100, 80, crop_at_edge, 160, 100), {30, 0, 100, 100});
expect("zero source falls back to destination", mkwvk::present_rect(0, 0, nullptr, 160, 100), {0, 0, 160, 100});
return failures ? 1 : 0;
}
4 changes: 3 additions & 1 deletion gpu/vk/vk_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
extern "C" {
#endif

#define MKWVK_API_VERSION 3
#define MKWVK_API_VERSION 4

/* (hookshot) the controls' names (host/nn_hid.c, host/pad_sdl.c, the pad_ and key_ lines of hookshot_options.ini):
the Switch's buttons in nn::hid's bit order, and a gamepad's inputs by position, SDL's order (the bottom face
Expand Down Expand Up @@ -76,6 +76,8 @@ typedef struct MkwVkHost {
do (Windows key codes, 0 none; slots 0 and 1). bind with button -1: all back to their defaults */
int (*binding)(int set, int button, int slot);
void (*bind)(int set, int button, int slot, int code);
/// The guest camera aspect explicitly applied at startup (0: its default aspect)
float (*aspect_ratio)(void);
} MkwVkHost;

/* what the back end gives the executable: the rx_* functions (gpu/render/rx_nvn.h, rx_core) */
Expand Down
43 changes: 43 additions & 0 deletions gpu/vk/vk_present_rect.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// The content rectangle for a window present, independent of Vulkan.
#pragma once

#include <cstdint>

namespace mkwvk {

struct PresentRect {
uint32_t x, y, w, h;
};

struct PresentContentSize {
uint32_t w, h;
};

constexpr PresentContentSize present_content_size(uint32_t sw, uint32_t sh, const int crop[4]) noexcept {
if (!sw || !sh) return {0, 0};
if (!crop || crop[2] <= 0 || crop[3] <= 0) return {sw, sh};
if (crop[0] < 0 || crop[1] < 0 || (uint32_t)crop[0] >= sw || (uint32_t)crop[1] >= sh) return {0, 0};
const uint32_t available_w = sw - (uint32_t)crop[0];
const uint32_t available_h = sh - (uint32_t)crop[1];
const uint32_t crop_w = (uint32_t)crop[2];
const uint32_t crop_h = (uint32_t)crop[3];
return {crop_w < available_w ? crop_w : available_w, crop_h < available_h ? crop_h : available_h};
}

constexpr PresentRect present_rect(uint32_t sw, uint32_t sh, const int crop[4], uint32_t dw, uint32_t dh,
bool preserve_stretch = false) noexcept {
if (!dw || !dh) return {0, 0, dw, dh};
if (preserve_stretch) return {0, 0, dw, dh};
const PresentContentSize content = present_content_size(sw, sh, crop);
if (!content.w || !content.h) return {0, 0, dw, dh};

// Fit the source content into the destination while preserving its aspect.
if ((uint64_t)dw * content.h > (uint64_t)dh * content.w) {
const uint32_t w = (uint32_t)((uint64_t)dh * content.w / content.h);
return {(dw - w) / 2, 0, w ? w : 1, dh};
}
const uint32_t h = (uint32_t)((uint64_t)dw * content.h / content.w);
return {0, (dh - h) / 2, dw, h ? h : 1};
}

} // namespace mkwvk
34 changes: 25 additions & 9 deletions gpu/vk/vk_replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "vk_state.h"
#include "vk_texture.h"
#include "vk_xfer.h"
#include "vk_present_rect.h"

#include "vk_internal.h"

Expand Down Expand Up @@ -1928,20 +1929,27 @@ void image_barrier(VkCommandBuffer cmd, VkImage image, VkImageLayout from, VkIma
vkCmdPipelineBarrier2(cmd, &d);
}

/// The crop of src (x, y, w, h; w 0: all of it) scaled into dst (dw x dh)
void blit_crop(VkCommandBuffer cmd, VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkImage dst,
VkImageLayout dst_layout, uint32_t dw, uint32_t dh) {
/// The crop of src (x, y, w, h; w 0: all of it) scaled into a rectangle of dst.
void blit_crop_to_rect(VkCommandBuffer cmd, VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkImage dst,
VkImageLayout dst_layout, PresentRect rect) {
int x0 = 0, y0 = 0, x1 = (int)sw, y1 = (int)sh;
if (crop && crop[2] > 0 && crop[3] > 0) x0 = crop[0], y0 = crop[1], x1 = crop[0] + crop[2], y1 = crop[1] + crop[3];
VkImageBlit b{};
b.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
b.srcOffsets[0] = {x0, y0, 0};
b.srcOffsets[1] = {std::min(x1, (int)sw), std::min(y1, (int)sh), 1};
b.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
b.dstOffsets[1] = {(int)dw, (int)dh, 1};
b.dstOffsets[0] = {(int)rect.x, (int)rect.y, 0};
b.dstOffsets[1] = {(int)(rect.x + rect.w), (int)(rect.y + rect.h), 1};
vkCmdBlitImage(cmd, src, VK_IMAGE_LAYOUT_GENERAL, dst, dst_layout, 1, &b, VK_FILTER_LINEAR);
}

/// The crop of src scaled to the whole destination (captures keep their existing dimensions).
void blit_crop(VkCommandBuffer cmd, VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkImage dst,
VkImageLayout dst_layout, uint32_t dw, uint32_t dh) {
blit_crop_to_rect(cmd, src, sw, sh, crop, dst, dst_layout, {0, 0, dw, dh});
}

// ---- PNG (stored deflate blocks: no compression library needed) ----
uint32_t crc_table[256];
uint32_t crc32(const uint8_t* p, size_t n, uint32_t c = 0xFFFFFFFFu) {
Expand Down Expand Up @@ -2631,12 +2639,20 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI
VkImage dst = dev.swap_images[index];
full_barrier(f.cmd); // (after the game's work for the frame: earlier submits on this queue)
image_barrier(f.cmd, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
if (const VkImage scaled = src && view ? framegen_scale(f.cmd, view, src, sw, sh, crop, dev.swap_extent.width, dev.swap_extent.height) : VK_NULL_HANDLE) {
blit_crop(f.cmd, scaled, dev.swap_extent.width, dev.swap_extent.height, nullptr, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
dev.swap_extent.width, dev.swap_extent.height);
const bool preserve_stretch = host && host->aspect_ratio && host->aspect_ratio() > 1.0f;
const PresentRect rect = present_rect(sw, sh, crop, dev.swap_extent.width, dev.swap_extent.height, preserve_stretch);
const bool has_bars = rect.x || rect.y || rect.w != dev.swap_extent.width || rect.h != dev.swap_extent.height;
if (has_bars) {
const VkClearColorValue black{0.0f, 0.0f, 0.0f, 1.0f};
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(f.cmd, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range);
// The following blit overlaps the clear in the content rectangle.
image_barrier(f.cmd, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
}
if (const VkImage scaled = src && view ? framegen_scale(f.cmd, view, src, sw, sh, crop, rect.w, rect.h) : VK_NULL_HANDLE) {
blit_crop_to_rect(f.cmd, scaled, rect.w, rect.h, nullptr, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rect);
} else if (src) {
blit_crop(f.cmd, src, sw, sh, crop, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dev.swap_extent.width,
dev.swap_extent.height);
blit_crop_to_rect(f.cmd, src, sw, sh, crop, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rect);
} else {
const VkClearColorValue black{};
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
Expand Down
4 changes: 3 additions & 1 deletion host/rx_backend.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ int hos_gpu_pools(uint64_t (*out)[2], int max);
uint32_t hos_gpu_maps_generation(void);
uint32_t nvn_vmap_generation(void);
uint64_t hos_reserved_cpus(void); /* hos_thread.c */
extern float hos_aspect_ratio; /* hos_main.c: the guest camera aspect applied at startup, or 0 */
uint32_t hos_pad_raw(void); /* nn_hid.c: the gamepads and the controls, for the Insert menu */
int hos_binding(int set, int button, int slot);
void hos_bind(int set, int button, int slot, int code);
Expand Down Expand Up @@ -132,13 +133,14 @@ static uint64_t host_guest_alloc(uint64_t size) {

void hos_watch_guest_writes(const uint8_t *data, uint64_t size); /* nvn_native.c */
static void host_watch_guest_writes(uint64_t guest, uint64_t size) { hos_watch_guest_writes(hos_guest_base() + guest, size); }
static float host_aspect_ratio(void) { return hos_aspect_ratio; }

static const MkwVkHost vk_host = {
MKWVK_API_VERSION, host_log, hos_guest_base, hos_guest_size, hos_gpu_ptr, nvn_gpu_guest, nvn_gpu_read,
nvn_gpu_write, host_collect, host_free_pages, host_mark_written, hos_ticks, hos_now_ns, nvn_tex_layout,
nvn_tex_level_offset, nvn_fmt_bpb, nvn_tex_level_tiles, nvn_tex_linear_stride, nvn_depth_minus_w, hos_gpu_pools,
hos_gpu_maps_generation, nvn_vmap_generation, hos_reserved_cpus, host_collect_in, host_guest_alloc,
host_watch_guest_writes, hos_pad_raw, hos_binding, hos_bind,
host_watch_guest_writes, hos_pad_raw, hos_binding, hos_bind, host_aspect_ratio,
};

static const MkwVkRenderer *vk; /* the Vulkan back end, when it was chosen and started */
Expand Down
Loading