From 09bab3f373d280fa996f3a1fae17d04980c733ec Mon Sep 17 00:00:00 2001 From: oscarlosu-lab <251827664+oscarlosu-lab@users.noreply.github.com> Date: Sun, 8 Nov 2026 05:32:29 +0100 Subject: [PATCH] feat(vk): add centered 3D rotating Triforce loading screen with retro LOADING... overlay --- gpu/vk/vk_api.h | 4 ++ gpu/vk/vk_draw.cpp | 6 ++ gpu/vk/vk_draw.h | 1 + gpu/vk/vk_framegen.cpp | 12 +++- gpu/vk/vk_menu.cpp | 132 +++++++++++++++++++++++++++++++++++++++-- gpu/vk/vk_menu.h | 4 ++ gpu/vk/vk_replay.cpp | 23 ++++++- host/hos.h | 3 + host/nn_fs.c | 25 ++++++++ host/rx_backend.c | 4 +- 10 files changed, 206 insertions(+), 8 deletions(-) diff --git a/gpu/vk/vk_api.h b/gpu/vk/vk_api.h index 32a2a6cc..62d8ff3c 100644 --- a/gpu/vk/vk_api.h +++ b/gpu/vk/vk_api.h @@ -94,6 +94,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 diff --git a/gpu/vk/vk_draw.cpp b/gpu/vk/vk_draw.cpp index 57a65a9d..2007f918 100644 --- a/gpu/vk/vk_draw.cpp +++ b/gpu/vk/vk_draw.cpp @@ -3856,10 +3856,13 @@ void probe_log(uint64_t f) { // (amd-fix, diagnosis) in a frame dump, the indirect mesh draws' arguments as the GPU has them: noted at the draw (inside // the pass, where no copy can be recorded) and copied out once the pass ends ("dump buffer ... mesh indirect") thread_local std::vector> dump_mesh_indirect; // (program key, indirect address) +std::atomic st_3d_draws{0}; void end_pass(VkCommandBuffer cmd) { if (!pass.open) return; vkCmdEndRendering(cmd); pass.open = false; + if (pass.depth && pass.width >= 320 && pass.height >= 240) + st_3d_draws.fetch_add(pass.draws, std::memory_order_relaxed); if (dump_on() && !dump_mesh_indirect.empty()) { for (const auto& [key, addr] : dump_mesh_indirect) { const BufferRef r = gpu_buffer_bind(addr, 16, false); @@ -6696,6 +6699,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; diff --git a/gpu/vk/vk_draw.h b/gpu/vk/vk_draw.h index 0037fc8c..00ffe417 100644 --- a/gpu/vk/vk_draw.h +++ b/gpu/vk/vk_draw.h @@ -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=,...: 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) diff --git a/gpu/vk/vk_framegen.cpp b/gpu/vk/vk_framegen.cpp index bf2fd982..5283f1aa 100644 --- a/gpu/vk/vk_framegen.cpp +++ b/gpu/vk/vk_framegen.cpp @@ -283,7 +283,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(); } diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 6d10a766..e766c812 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -11,6 +11,7 @@ // choice's or an amount's left or right half, Left or Right) and a right click goes back. vk_menu_hit.h has the layout. #include #include +#include #include #include #include @@ -198,9 +199,11 @@ Row rows[] = { "Bounds-checked buffer reads: a read past a buffer's end is dropped instead of stopping the GPU. Auto is on, on every card; " "Off can be faster on some NVIDIA cards. Applies after restart."}, - // Gameplay (3 rows) + // Gameplay (4 rows) {TAB_GAMEPLAY, "FPS counter", &opts.show_fps, 0, 1, 1, tx_onoff, "Shows frames per second in the top right corner. With frame generation on, it also shows the game's own rate."}, + {TAB_GAMEPLAY, "Loading animation", &opts.loading_screen, 0, 1, 1, tx_onoff, + "Displays an animated golden 3D Triforce during zone transitions."}, {TAB_GAMEPLAY, "Game speed", &opts.realtime, 0, 1, 1, tx_speed, "Per frame: the game slows down whenever it runs below 60 fps. Real time: it keeps normal speed down to 15 fps by taking " "longer steps."}, @@ -458,7 +461,8 @@ void set_row(Row& r, int v, int dir, bool keep = true) { *r.value = v > r.hi ? r.lo : v < r.lo ? r.hi : v; if (r.value != &opts.fullscreen && r.value != &opts.show_fps && r.value != &opts.gpu_device && r.value != &opts.invert_cam_x && r.value != &opts.invert_cam_y && r.value != &opts.mouse_native && - r.value != &opts.mouse_sens && r.value != &opts.mouse_invert) { // (the controls are not the preset's) + r.value != &opts.mouse_sens && r.value != &opts.mouse_invert && + r.value != &opts.loading_screen) { // (the controls are not the preset's) opts.preset = 0; // Custom sync_preset_budgets(0); } @@ -571,7 +575,7 @@ struct Bitmap { bool keyed = false; // (black is see-through: copied as the runs of pixels in between) std::vector 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); } @@ -991,6 +995,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, in pixels: the menu keeps that height whichever row is selected int measure_help(int s) { HDC dc = CreateCompatibleDC(nullptr); @@ -1032,7 +1127,7 @@ struct Upload { void* map = nullptr; uint64_t size = 0, version = 0; }; -Upload ups[2][3]; // (the menu's, the counter's; a frame in flight's each) +Upload ups[3][3]; // (0: menu, 1: fps counter, 2: 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); @@ -1135,6 +1230,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 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_frame(bool game_frame) { shown_frames.fetch_add(1, std::memory_order_relaxed); if (game_frame) game_frames.fetch_add(1, std::memory_order_relaxed); @@ -1636,6 +1735,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) { @@ -1669,4 +1777,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[2][slot], (int)(w - lw) / 2, (int)(h - lh) / 2, w, h); + } +} + } // namespace mkwvk diff --git a/gpu/vk/vk_menu.h b/gpu/vk/vk_menu.h index 75aa3d38..b4cfb918 100644 --- a/gpu/vk/vk_menu.h +++ b/gpu/vk/vk_menu.h @@ -28,6 +28,7 @@ struct Options { int invert_cam_y = 0; // right-stick camera vertical inversion: 0 normal, 1 inverted int pad_layout = 0; // 0 Nintendo (default), 1 Xbox / PC, 2 Swap Face Buttons, 3 Custom 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 @@ -42,6 +43,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) @@ -52,5 +55,6 @@ int menu_take_cheat_action(); void menu_note_frame(bool game_frame); // (the frame rates the FPS counter shows) /// 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); } // namespace mkwvk diff --git a/gpu/vk/vk_replay.cpp b/gpu/vk/vk_replay.cpp index d4c0d8bc..b19d9e8a 100644 --- a/gpu/vk/vk_replay.cpp +++ b/gpu/vk/vk_replay.cpp @@ -2503,6 +2503,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); @@ -3012,7 +3028,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); diff --git a/host/hos.h b/host/hos.h index b4fb63b4..e74416c6 100644 --- a/host/hos.h +++ b/host/hos.h @@ -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 diff --git a/host/nn_fs.c b/host/nn_fs.c index e42e54c9..05cf8418 100644 --- a/host/nn_fs.c +++ b/host/nn_fs.c @@ -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) { @@ -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); @@ -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; diff --git a/host/rx_backend.c b/host/rx_backend.c index 18e8605d..4e87fb73 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -155,8 +155,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, - hos_mouse_pointer, + 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, }; static const MkwVkRenderer *vk; /* the Vulkan back end, when it was chosen and started */