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
4 changes: 4 additions & 0 deletions gpu/vk/vk_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ typedef struct MkwVkHost {
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);
/// Returns 1 when a scene or zone is loading from storage, 0 otherwise
int (*is_loading)(void);
/// Clears the loading state once the new 3D scene begins rendering
void (*clear_loading)(void);
int (*take_key_press)(int key);
/* (hookshot) the free mouse (MkwVkPointer), read and its presses and wheel cleared: 1 when the cursor is over the
window and the window has the focus. The Insert menu releases the mouse while it is open: WM_APP + 8 to the
Expand Down
6 changes: 6 additions & 0 deletions gpu/vk/vk_draw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3903,9 +3903,12 @@ void probe_log(uint64_t f) {
namespace {
TLS_OBJECT(dump_mesh_indirect, std::vector<std::pair<uint64_t, uint64_t>>) // (program key, indirect address)
} // namespace
std::atomic<int64_t> st_3d_draws{0};
void end_pass(VkCommandBuffer cmd) {
if (!pass().open) return;
vkCmdEndRendering(cmd);
if (pass().depth && pass().width >= 320 && pass().height >= 240)
st_3d_draws.fetch_add(pass().draws, std::memory_order_relaxed);
pass().open = false;
if (dump_on() && !dump_mesh_indirect().empty()) {
for (const auto& [key, addr] : dump_mesh_indirect()) {
Expand Down Expand Up @@ -6901,6 +6904,9 @@ void draw_records_log(uint64_t lo, uint64_t hi) {
int64_t draw_desc_reused() {
return st_desc_reused;
}
int64_t draw_3d_take() {
return st_3d_draws.exchange(0, std::memory_order_relaxed);
}

void pipeline_cache_save(bool force) {
if (!pcache || (!force && pcache_built_since_save < 32)) return;
Expand Down
1 change: 1 addition & 0 deletions gpu/vk/vk_draw.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ void draw_stats2(int64_t* passes, int64_t* mesh, double* pipeline_ms);
void draw_stats3(int64_t* pending_skips, int64_t* waits, double* wait_ms);
void draw_skip_report(double frames); // (hookshot) skips by the line that made them
int64_t draw_desc_reused();
int64_t draw_3d_take();
/// RX_VK_FLASH_RECORD=<key>,...: the last frames' draws of those programs, written to the log for frames around a flash
void draw_records_log(uint64_t frame_lo, uint64_t frame_hi);
void pass_traces_log(uint64_t frame_lo, uint64_t frame_hi); // (RX_VK_PASS_TRACE=1)
Expand Down
12 changes: 11 additions & 1 deletion gpu/vk/vk_framegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,17 @@ void display_main() {
Job j;
{
std::unique_lock lk(fg.mu);
fg.cv.wait(lk, [] { return !fg.jobs.empty(); });
while (fg.jobs.empty()) {
if (menu_loading_active()) {
if (!fg.cv.wait_for(lk, std::chrono::milliseconds(16), [] { return !fg.jobs.empty(); })) {
lk.unlock();
present_image(VK_NULL_HANDLE, 0, 0, nullptr, VK_NULL_HANDLE);
lk.lock();
}
} else {
fg.cv.wait(lk, [] { return !fg.jobs.empty(); });
}
}
j = fg.jobs.front();
fg.jobs.pop_front();
}
Expand Down
128 changes: 125 additions & 3 deletions gpu/vk/vk_menu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -474,9 +474,11 @@ Row rows[] = {
"Bounds-checked buffer reads: a read past a buffer's end is dropped instead of stopping the GPU. Applies after a "
"restart.", hn_robust, true},

// Gameplay (3 rows)
// Gameplay (4 rows)
{TAB_GAMEPLAY, "FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, "Frames per second in the top right corner.", hn_fps,
false, true},
{TAB_GAMEPLAY, "Loading animation", &opts.loading_screen, 0, 1, 1, tx_onoff,
"Displays an animated golden 3D Triforce during zone transitions.", nullptr, false, true},
{TAB_GAMEPLAY, "Game speed", &opts.realtime, 0, 1, 1, tx_speed, "How the game keeps time when it runs below 60 fps.",
hn_speed},
{TAB_GAMEPLAY, "Text speed", &opts.text_speed, 0, 3, 1, tx_text_speed,
Expand Down Expand Up @@ -988,7 +990,7 @@ struct Bitmap {
bool keyed = false; // (black is see-through: copied as the runs of pixels in between)
std::vector<VkBufferImageCopy> runs;
};
Bitmap menu_bmp, fps_bmp;
Bitmap menu_bmp, fps_bmp, loading_bmp;
std::mutex bmp_mu;
int scale_of(uint32_t h) { return std::max(1u, h / 720); }

Expand Down Expand Up @@ -1466,6 +1468,97 @@ void paint_fps(HDC dc, uint32_t w, uint32_t h, int s) {
DeleteObject(label);
DeleteObject(big);
}
const char* loading_text() {
return "LOADING...";
}
static void fill_triangle(HDC dc, POINT p0, POINT p1, POINT p2, COLORREF color) {
if (p0.y > p1.y) std::swap(p0, p1);
if (p1.y > p2.y) std::swap(p1, p2);
if (p0.y > p1.y) std::swap(p0, p1);

const int total_h = p2.y - p0.y;
if (total_h <= 0) return;

HBRUSH brush = CreateSolidBrush(color);
for (int y = p0.y; y <= p2.y; y++) {
const bool second_half = y > p1.y || p1.y == p0.y;
const int segment_h = second_half ? (p2.y - p1.y) : (p1.y - p0.y);
if (segment_h <= 0) continue;

const float alpha = (float)(y - p0.y) / (float)total_h;
const float beta = (float)(y - (second_half ? p1.y : p0.y)) / (float)segment_h;

int ax = p0.x + (int)std::round((p2.x - p0.x) * alpha);
int bx = second_half ? (p1.x + (int)std::round((p2.x - p1.x) * beta))
: (p0.x + (int)std::round((p1.x - p0.x) * beta));
if (ax > bx) std::swap(ax, bx);
RECT r{ax, y, bx + 1, y + 1};
FillRect(dc, &r, brush);
}
DeleteObject(brush);
}
void paint_loading(HDC dc, uint32_t w, uint32_t h, int s) {
fill(dc, 0, 0, (int)w, (int)h, RGB(0, 0, 0));
SetBkMode(dc, TRANSPARENT);

const double PI = 3.14159265358979323846;
double angle = (GetTickCount64() % 2400) * (2.0 * PI / 2400.0);
double cosA = cos(angle);
double absCos = fabs(cosA);
if (absCos < 0.06) absCos = 0.06;

int cx = (int)w / 2;
int cy = 60 * s;
int w_tri = 56 * s;
int h_tri = 48 * s;
int w_half = w_tri / 2;

auto project = [&](int x, int y) -> POINT {
int px = cx + (int)std::round((x - cx) * cosA);
return {px, y};
};

int r1 = std::clamp((int)(255 * (0.65 + 0.35 * absCos)), 0, 255);
int g1 = std::clamp((int)(220 * (0.65 + 0.35 * absCos)), 0, 255);
int b1 = std::clamp((int)(40 * (0.65 + 0.35 * absCos)), 0, 255);

int r2 = std::clamp((int)(255 * (0.58 + 0.42 * absCos)), 0, 255);
int g2 = std::clamp((int)(210 * (0.58 + 0.42 * absCos)), 0, 255);
int b2 = std::clamp((int)(30 * (0.58 + 0.42 * absCos)), 0, 255);

int r3 = std::clamp((int)(255 * (0.72 + 0.28 * absCos)), 0, 255);
int g3 = std::clamp((int)(230 * (0.72 + 0.28 * absCos)), 0, 255);
int b3 = std::clamp((int)(50 * (0.72 + 0.28 * absCos)), 0, 255);

POINT top_pts[3] = {
project(cx, cy - h_tri),
project(cx - w_half, cy),
project(cx + w_half, cy)
};
fill_triangle(dc, top_pts[0], top_pts[1], top_pts[2], RGB(r1, g1, b1));

POINT left_pts[3] = {
project(cx - w_half, cy),
project(cx - w_tri, cy + h_tri),
project(cx, cy + h_tri)
};
fill_triangle(dc, left_pts[0], left_pts[1], left_pts[2], RGB(r2, g2, b2));

POINT right_pts[3] = {
project(cx + w_half, cy),
project(cx, cy + h_tri),
project(cx + w_tri, cy + h_tri)
};
fill_triangle(dc, right_pts[0], right_pts[1], right_pts[2], RGB(r3, g3, b3));

const char* msg = loading_text();
HFONT font = hard_font(14 * s, FW_BOLD);
HGDIOBJ old_font = SelectObject(dc, font);
SetTextAlign(dc, TA_CENTER | TA_TOP);
outlined(dc, cx, cy + h_tri + 16 * s, msg, RGB(255, 225, 90), s);
SelectObject(dc, old_font);
DeleteObject(font);
}
/// The tallest help text at scale s, a row's help and its value's line, in pixels: the menu keeps that height whichever
/// row is selected and whatever its value
int measure_help(int s) {
Expand Down Expand Up @@ -1531,7 +1624,7 @@ struct Upload {
void* map = nullptr;
uint64_t size = 0, version = 0;
};
Upload ups[3][3]; // (the menu's, the counter's, the start-up screen's; a frame in flight's each)
Upload ups[4][3]; // (0: menu, 1: counter, 2: start-up screen, 3: loading animation; a frame in flight's each)

void copy_over(VkCommandBuffer cmd, VkImage dst, Bitmap& b, Upload& u, int x, int y, uint32_t w, uint32_t h) {
std::lock_guard lk(bmp_mu);
Expand Down Expand Up @@ -1678,6 +1771,10 @@ int menu_player_flight() { return cheats_enabled ? player_flight.load(std::memor
int menu_free_camera() { return free_camera.load(std::memory_order_relaxed); }
int menu_take_cheat_action() { return cheat_action.exchange(0, std::memory_order_relaxed); }

std::atomic<bool> loading_active{false};
void menu_set_loading_active(bool active) { loading_active.store(active, std::memory_order_relaxed); }
bool menu_loading_active() { return opts.loading_screen && loading_active.load(std::memory_order_relaxed); }

void menu_note_present_sizes(uint32_t picture_w, uint32_t picture_h, uint32_t window_w, uint32_t window_h) {
const auto c = [](uint32_t v) { return (uint64_t)std::min(v, 0xffffu); };
present_sizes.store(c(picture_w) << 48 | c(picture_h) << 32 | c(window_w) << 16 | c(window_h), std::memory_order_relaxed);
Expand Down Expand Up @@ -2216,6 +2313,15 @@ void menu_tick() {
gdi_draw(fps_bmp, 120u * scale, 54u * scale, paint_fps, scale);
}
}
if (menu_loading_active()) {
static uint64_t last_load_draw = 0;
uint64_t now_tick = GetTickCount64();
if (now_tick - last_load_draw >= 16) {
last_load_draw = now_tick;
loading_bmp.keyed = true;
gdi_draw(loading_bmp, 260u * scale, 180u * scale, paint_loading, scale);
}
}
}

void menu_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_t h) {
Expand Down Expand Up @@ -2261,4 +2367,20 @@ void menu_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_
}
}

void loading_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_t h) {
if (!menu_loading_active()) return;
std::unique_lock lk(bmp_mu);
const uint32_t lw = loading_bmp.w, lh = loading_bmp.h;
lk.unlock();
if (lw && lh) {
VkMemoryBarrier2 mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2};
mb.srcStageMask = mb.dstStageMask = VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT;
mb.srcAccessMask = mb.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
VkDependencyInfo di{VK_STRUCTURE_TYPE_DEPENDENCY_INFO};
di.memoryBarrierCount = 1, di.pMemoryBarriers = &mb;
vkCmdPipelineBarrier2(cmd, &di);
copy_over(cmd, dst, loading_bmp, ups[3][slot], (int)(w - lw) / 2, (int)(h - lh) / 2, w, h);
}
}

} // namespace mkwvk
4 changes: 4 additions & 0 deletions gpu/vk/vk_menu.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct Options {
int gyro_inv_x = 0; // the motion aim's horizontal turned around
int gyro_inv_y = 0; // the motion aim's vertical turned around
int robust = 0; // bounds-checked buffer access: 0 Auto (on), 1 Off, 2 On (gpu/vk/vk_main.cpp; RX_VK_ROBUST)
int loading_screen = 1; // animated 3D Triforce loading screen: 0 Off, 1 On
int preset = 0; // quality preset: 0 Custom, 1 Minimum (4GB), 2 Low (6GB), 3 Balanced (8GB), 4 High (12GB), 5 Ultra (16GB+)
int vram_mb = 0; // dynamic texture cache budget limit in MB
int mirror_mb = 0; // staging mirror limit in MB
Expand All @@ -55,6 +56,8 @@ void menu_init(); // (the options from the file, once)
void menu_set_gpu_count(int count);
void menu_tick(); // (each frame shown: the keys, while the game's window has the focus)
bool menu_is_open();
void menu_set_loading_active(bool active);
bool menu_loading_active();
bool resolution_auto(); // (the game's dynamic resolution: Resolution Auto)
int resolution_scale(); // (the fixed path's scale in 1/10000: the Resolution row's, as a twelfth of the output)
void menu_option_set(int id, int value); // (from the game's side: 7 the mouse's sensitivity, the - and = keys)
Expand All @@ -68,6 +71,7 @@ void menu_note_frame(bool game_frame); // (the frame rates the FPS counter sho
void menu_note_present_sizes(uint32_t picture_w, uint32_t picture_h, uint32_t window_w, uint32_t window_h);
/// Over the window's image (TRANSFER_DST_OPTIMAL), w x h: the menu and the FPS counter. slot: the frame in flight, 0 to 2.
void menu_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_t h);
void loading_record(VkCommandBuffer cmd, VkImage dst, int slot, uint32_t w, uint32_t h);
/// (hookshot) The start-up screen while the shaders are prepared (vk_main.cpp, vk_startup_paint.h), drawn by menu_record under the
/// counter and the menu. Null takes it away.
struct StartupView;
Expand Down
23 changes: 22 additions & 1 deletion gpu/vk/vk_replay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2596,6 +2596,22 @@ void do_present(const nvn_tex* t, const int crop[4]) {
{
std::lock_guard lk(replay_mu);
bench_frame(presents);
{
int64_t cur_d = 0, cur_sk = 0, cur_di = 0, cur_pl = 0;
draw_stats(&cur_d, &cur_sk, &cur_di, &cur_pl);
static int64_t prev_d = 0, prev_di = 0;
const int64_t f_draws = cur_d - prev_d, f_disp = cur_di - prev_di;
prev_d = cur_d, prev_di = cur_di;
const int64_t d3d = draw_3d_take();
const bool was_loading = menu_loading_active();
const int64_t min_draws = was_loading ? 1 : 3;
if (opts.loading_screen && presents > 60 && f_disp == 0 && d3d < 15 && f_draws >= min_draws && f_draws <= 25) {
menu_set_loading_active(true);
} else if (was_loading) {
menu_set_loading_active(false);
if (host && host->clear_loading) host->clear_loading();
}
}
SlowWatch slow("present");
frame_submits.clear();
PROF(PROF_PRESENT);
Expand Down Expand Up @@ -3121,7 +3137,12 @@ void present_image(VkImage src, uint32_t sw, uint32_t sh, const int crop[4], VkI
// 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) {
if (menu_loading_active()) {
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);
image_barrier(f.cmd, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
} else 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_to_rect(f.cmd, src, sw, sh, crop, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rect);
Expand Down
3 changes: 3 additions & 0 deletions host/hos.h
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,7 @@ uint32_t movie_reader_codec(MovieReader *r); /* the stream's FourCC: 'VP90', '
uint64_t hos_ticks(void);
void hos_sleep_ns(int64_t ns);
int64_t hos_now_ns(void);
void hos_mark_loading(void);
void hos_clear_loading(void);
int hos_is_loading(void);
#endif
25 changes: 25 additions & 0 deletions host/nn_fs.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@ static void mount(const char *name, const char *root, int writable) {
int hos_mods_romfs(const char *rel, char *out, size_t cap); /* mods.c */
int hos_mods_dir_extras(const char *rel, const char *base_dir, void (*fn)(void *, const char *, int, uint64_t), void *arg);

static volatile uint64_t hos_last_loading_tick = 0;

void hos_mark_loading(void) {
hos_last_loading_tick = GetTickCount64();
}

void hos_clear_loading(void) {
hos_last_loading_tick = 0;
}

int hos_is_loading(void) {
uint64_t t = hos_last_loading_tick;
if (!t) return 0;
uint64_t now = GetTickCount64();
return (now >= t && now - t < 5000) ? 1 : 0;
}

/* "mount:/a/b" -> host path (a mod's file instead of the RomFS one, when a mod has it: mods.c). Returns 0 if the mount
is unknown. */
static int host_path(const char *guest, char *out, size_t cap, int *writable) {
Expand Down Expand Up @@ -247,6 +264,10 @@ HLE(_ZN2nn2fs8OpenFileEPNS0_10FileHandleEPKci) {
fh->h = h;
fh->writable = want_write;
snprintf(fh->path, sizeof fh->path, "%s", g);
if (!want_write && (strstr(g, "Banc/") || strstr(g, "WorldParam/") ||
strstr(g, "PrepareScreensOfLoading") || strstr(g, "LoadBanc"))) {
hos_mark_loading();
}
G64(ARG(0)) = new_handle(fh);
if (hos_trace || fs_trace()) hos_log("fs: OpenFile(%s)", g);
RET(0);
Expand All @@ -267,6 +288,10 @@ static uint64_t read_at(Ctx *c, uint64_t id, int64_t off, uint64_t buf, uint64_t
hos_sleep_ns(delay_ns);
}
const int away = hos_vcore_away(); /* (as OpenFile: no core held through the read) */
if (size >= 32768 && hos_is_loading() && !strstr(fh->path, "Sound/") && !strstr(fh->path, "Movie/") &&
!strstr(fh->path, "PreComputedCulling/") && !strstr(fh->path, "Bake/")) {
hos_mark_loading();
}
while (done < size) {
OVERLAPPED ov = {0};
int64_t o = off + (int64_t)done;
Expand Down
3 changes: 2 additions & 1 deletion host/rx_backend.c
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ static const MkwVkHost vk_host = {
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_aspect_ratio, host_take_key_press,
host_watch_guest_writes, hos_pad_raw, hos_binding, hos_bind, host_aspect_ratio,
hos_is_loading, hos_clear_loading, host_take_key_press,
hos_mouse_pointer, hos_mic_count, hos_mic_name, hos_mic_select, hos_sdl_gyro_status,
};

Expand Down
Loading