From 4e3280c2a733af0ce8dbb505773452e36d237503 Mon Sep 17 00:00:00 2001 From: epicmeme546 <178368684+epicmeme546@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:53:59 -0500 Subject: [PATCH] Make debug-menu teleports always use a start position the area lists, move Link on when he falls through the ground after arriving, and keep the free camera above the ground. A teleport without a start position, or with one the area does not list, put Link where nothing held him: in Kakariko Village he fell for good, and the game refuses events, teleports included, while Link is in the air. The game's list has an empty name in 45 areas, which the Teleport tab listed as a default start; those entries are left out, and such a request takes the area's first listed position. For a few seconds after arriving, Link falling 25 m below where he arrived is taken for a fall through the ground, and the area's next start position is asked for, up to three times. The free camera's left stick moved it along its view, so a camera looking down flew into the ground, and below it the picture showed the void under the map, which looks like ground the game failed to draw. The stick now moves it level, only ZR and ZL change its height, and with DLSS, DLAA or FSR 3.1 or 4 on, nine of the view distances the upscaler's motion pass works out (vk_dlaa.c fcam_probe) come back each frame and the camera rises where the ground ahead or below is within a clearance that grows with its speed. Flown at 25 m/s two metres above Hyrule Field, it went over every hill. For test runs, NX2NAVI_DBG_TEST gains route and routeto: Link is steered through points, or through the area's own start positions, from the way he moves, and sidesteps where he stops gaining ground. The start positions' coordinates are read from the same file as their names. Co-Authored-By: Claude Opus 5.5 --- runtime/debug_tools.c | 311 ++++++++++++++++++++++++++++++++++++-- runtime/debug_tools.h | 4 +- runtime/ui/debug_menu.cpp | 9 +- runtime/vk_dlaa.c | 51 +++++++ 4 files changed, 357 insertions(+), 18 deletions(-) diff --git a/runtime/debug_tools.c b/runtime/debug_tools.c index f4f393d3..88abf7d9 100644 --- a/runtime/debug_tools.c +++ b/runtime/debug_tools.c @@ -31,6 +31,7 @@ // // The free camera stands in for the game's camera inside the game's own camera update (dbg_freecam_host), so the game // itself draws from it. +#include #include #include #include @@ -279,8 +280,13 @@ static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, } // ---- the places to teleport to and the events, from the player's game files (names_thread) -typedef struct { const char *area, *pos; } Place; +typedef struct { + const char *area, *pos; + float xyz[3]; // where it is in the area, metres, when the file gives it (has_xyz) + int has_xyz; +} Place; static Place *places; // by area, as the game's file lists them; the texts stay in the file's buffer +static const char *place_xyz_key; // the key the positions' coordinates were found under, for the log static int place_n; static char **event_names; // the event files that hold a script, by name static int event_n; @@ -296,6 +302,38 @@ static const char *byml_key(const uint8_t *p, size_t n, uint32_t keys, uint32_t return (const char *)p + keys + off; } +// Three floats at node off of the given kind: an array of three floats (node 0xc0: its count, the kinds of its items +// padded to four bytes, then the items; a float is kind 0xd2, its bits in place), or found in a dictionary under the +// first key that holds one, a key whose name says where (Translate, Trans, Pos) taken before the others. depth bounds +// the search. *key: the key it was found under. +static int byml_vec3(const uint8_t *p, size_t n, uint32_t keys, uint32_t off, uint8_t kind, float out[3], int depth, + const char **key) { + if ((size_t)off + 4 > n) return 0; + uint32_t count = u24(p + off + 1); + if (kind == 0xc0 && p[off] == 0xc0 && count == 3) { + uint32_t vals = off + 4 + ((count + 3) & ~3u); + if ((size_t)vals + 12 > n || p[off + 4] != 0xd2 || p[off + 5] != 0xd2 || p[off + 6] != 0xd2) return 0; + memcpy(out, p + vals, 12); + return isfinite(out[0]) && isfinite(out[1]) && isfinite(out[2]); + } + if (kind != 0xc1 || p[off] != 0xc1 || depth <= 0) return 0; + int found = 0; + for (int pass = 0; pass < 2 && !found; pass++) + for (uint32_t i = 0; i < count && !found && (size_t)off + 12 + 8 * (size_t)i <= n; i++) { + const uint8_t *e = p + off + 4 + 8 * (size_t)i; + const char *name = byml_key(p, n, keys, u24(e)); + int named = name && (strstr(name, "Trans") || strstr(name, "Pos")); + if (pass == 0 && !named) continue; + uint32_t at; + memcpy(&at, e + 4, 4); + if (byml_vec3(p, n, keys, at, e[3], out, depth - 1, key)) { + if (key && name && depth == 2) *key = name; + found = 1; + } + } + return found; +} + // The start positions file: a dictionary (node 0xc1: its count, then entries of a key's number in 24 bits, the // value's kind and the value's offset) of areas, each a dictionary of its start positions. static void places_in(const uint8_t *p, size_t n) { @@ -314,8 +352,14 @@ static void places_in(const uint8_t *p, size_t n) { if (!area || e[3] != 0xc1 || (size_t)at + 4 > n || p[at] != 0xc1 || strlen(area) > 63) continue; uint32_t spots = u24(p + at + 1); for (uint32_t k = 0; k < spots && count < 4096 && (size_t)at + 12 + 8 * (size_t)k <= n; k++) { - const char *pos = byml_key(p, n, keys, u24(p + at + 4 + 8 * (size_t)k)); - if (pos && strlen(pos) <= 63) list[count++] = (Place){area, pos}; + const uint8_t *pe = p + at + 4 + 8 * (size_t)k; + const char *pos = byml_key(p, n, keys, u24(pe)); + if (!pos || !pos[0] || strlen(pos) > 63) continue; + Place pl = {area, pos, {0, 0, 0}, 0}; + uint32_t val; + memcpy(&val, pe + 4, 4); + pl.has_xyz = byml_vec3(p, n, keys, val, pe[3], pl.xyz, 2, &place_xyz_key); + list[count++] = pl; } } places = list, place_n = count; @@ -369,8 +413,15 @@ static void names_thread(void *arg) { ZSTD_freeDDict(dd); free(dics); free(dz); - rt_log("debug: names for %d of %d flags, from %d of the game's event files, in %.0f ms; %d events, %d places to teleport to", - names_found, flag_n, files, (double)(plat_now_ns() - t0) / 1e6, event_n, place_n); + int with_xyz = 0; + for (int i = 0; i < place_n; i++) with_xyz += places[i].has_xyz; + rt_log("debug: names for %d of %d flags, from %d of the game's event files, in %.0f ms; %d events, %d places to teleport to " + "(%d with their coordinates, under \"%s\")", names_found, flag_n, files, (double)(plat_now_ns() - t0) / 1e6, + event_n, place_n, with_xyz, place_xyz_key ? place_xyz_key : ""); + if (cfg_get("NX2NAVI_DBG_TEST")) // test runs: every place with its coordinates, to plan routes (dbg_route) + for (int i = 0; i < place_n; i++) + rt_log("debug: place %s > %s at %.1f %.1f %.1f%s", places[i].area, places[i].pos, places[i].xyz[0], + places[i].xyz[1], places[i].xyz[2], places[i].has_xyz ? "" : " (no coordinates)"); __atomic_store_n(&names_state, 2, __ATOMIC_RELEASE); } @@ -391,6 +442,28 @@ int dbg_names(int *found) { int dbg_places(void) { return __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? place_n : 0; } const char *dbg_place_area(int i) { return i >= 0 && i < dbg_places() ? places[i].area : ""; } const char *dbg_place_pos(int i) { return i >= 0 && i < dbg_places() ? places[i].pos : ""; } + +// The start position of area that comes after pos in the game's list, the first one for a pos the area does not list +// (or none given), NULL for an area without any. A change of area to a start position the area does not have puts +// Link where nothing holds him: he falls below the map for good, and the game refuses events while he falls. +static const char *place_after(const char *area, const char *pos) { + const char *first = NULL; + int seen = 0, n = dbg_places(); + for (int i = 0; i < n; i++) { + if (strcmp(places[i].area, area)) continue; + if (seen) return places[i].pos; + if (!first) first = places[i].pos; + if (pos && pos[0] && !strcmp(places[i].pos, pos)) seen = 1; + } + return first; +} + +static int place_listed(const char *area, const char *pos) { + int n = dbg_places(); + for (int i = 0; i < n; i++) + if (!strcmp(places[i].area, area) && !strcmp(places[i].pos, pos)) return 1; + return 0; +} int dbg_events(void) { return __atomic_load_n(&names_state, __ATOMIC_ACQUIRE) == 2 ? event_n : 0; } const char *dbg_event_name(int i) { return i >= 0 && i < dbg_events() ? event_names[i] : ""; } @@ -462,12 +535,21 @@ static int event_request(Cpu *c, const char *name) { } // ---- the teleport and the event start: asked for by the menu, done by warp_tick on the game's thread -enum { W_IDLE, W_ASKED, W_EVENT, W_FLIGHT }; +// W_SETTLE: Link arrived and is watched for a while; one who falls FALL_M below where he arrived has no ground under +// him, and the change of area is asked again for the area's next start position (place_after), up to FALL_TRIES times. +// The game refuses events while Link is in the air, so such a request is tried again every RETRY_EVERY ticks until +// RETRY_FOR ticks have passed. +enum { W_IDLE, W_ASKED, W_EVENT, W_FLIGHT, W_SETTLE }; +enum { SETTLE_TICKS = 180, FALL_TRIES = 3, RETRY_EVERY = 30, RETRY_FOR = 1200, LIST_WAIT = 600 }; +static const float FALL_M = 25.0f; // metres: an arrival's own drop is a step or two static struct { char area[64], pos[64], event[80]; // q_lock: what the menu asked for int state; // W_* uint64_t at; // the tick of the next step, or the one at which the wait for the arrival ends + uint64_t retry_until, waited; // W_ASKED: refused requests are tried again until then; ticks spent on the list float from[3]; // where Link stood + float land; // the height he arrived at + int tries; // start positions tried after falls } warp; static char status[160]; // q_lock: the last thing that happened, for the menu @@ -491,6 +573,7 @@ void dbg_warp(const char *area, const char *pos) { if (warp.state == W_IDLE && !warp.event[0] && area[0]) { snprintf(warp.area, sizeof warp.area, "%s", area); snprintf(warp.pos, sizeof warp.pos, "%s", pos); + warp.tries = 0, warp.retry_until = 0, warp.waited = 0, warp.at = 0; warp.state = W_ASKED; } plat_mutex_unlock(&q_lock); @@ -514,12 +597,35 @@ static void warp_tick(Cpu *c, uint64_t tick) { warp.event[0] = 0; plat_mutex_unlock(&q_lock); } - if (warp.state == W_ASKED) { + if (warp.state == W_ASKED && tick >= warp.at) { + // The area's own list of start positions decides the position: read with the flag names, which a test run that + // never opened the menu has not started yet + if (dbg_names(NULL) != 2 && warp.waited++ < LIST_WAIT) { + dbg_flags_refresh(), dbg_names(NULL); + return; + } + if (dbg_places() && !place_listed(warp.area, warp.pos)) { + const char *first = place_after(warp.area, NULL); + if (!first) { + status_set("%s is not an area of the game's list of start positions: no teleport.", warp.area, ""); + warp.state = W_IDLE; + return; + } + rt_log("debug: %s lists no start position \"%s\": %s, the first it lists, instead", warp.area, warp.pos, first); + plat_mutex_lock(&q_lock); + snprintf(warp.pos, sizeof warp.pos, "%s", first); + plat_mutex_unlock(&q_lock); + } // the game's event that only waits: the change of area is asked for while it runs if (player() && gmd() && dbg_player_pos(warp.from) && event_request(c, "DefaultParentEvent")) { warp.at = tick + 4, warp.state = W_EVENT; + } else if (tick < warp.retry_until) { + warp.at = tick + RETRY_EVERY; // Link still falls: the game takes it once he lands, or the time runs out } else { - status_set(REFUSED, "the teleport", ""); + status_set(warp.tries ? "The game refuses a teleport while Link falls below %s: load the save, or try again " + "should he land." + : REFUSED, + warp.tries ? warp.area : "the teleport", ""); warp.state = W_IDLE; } } else if (warp.state == W_EVENT && tick >= warp.at) { @@ -540,15 +646,34 @@ static void warp_tick(Cpu *c, uint64_t tick) { } } else if (warp.state == W_FLIGHT) { char now[64]; - float p[3]; + float p[3] = {0, 0, 0}; int there = text_get("Sequence_CurrentBanc", now) && !strcmp(now, warp.area) && dbg_player_pos(p) && fabsf(p[0] - warp.from[0]) + fabsf(p[1] - warp.from[1]) + fabsf(p[2] - warp.from[2]) > 0.5f; if (there || tick >= warp.at) { status_set(there ? "Arrived in %s, %s." : "The teleport to %s, %s was asked for; no arrival was seen.", warp.area, warp.pos[0] ? warp.pos : "its default start"); - warp.state = W_IDLE; + warp.state = there ? W_SETTLE : W_IDLE; + warp.land = p[1], warp.at = tick + SETTLE_TICKS; dbg_freecam_reset(); // a free camera left behind in the old area's place starts again from the game's view } + } else if (warp.state == W_SETTLE) { + float p[3]; + if (dbg_player_pos(p) && p[1] < warp.land - FALL_M) { + const char *next = place_after(warp.area, warp.pos); + if (warp.tries < FALL_TRIES && next && strcmp(next, warp.pos)) { + status_set("Link fell through the ground at %s: the next start position, %s...", warp.area, next); + plat_mutex_lock(&q_lock); + snprintf(warp.pos, sizeof warp.pos, "%s", next); + plat_mutex_unlock(&q_lock); + warp.tries++, warp.retry_until = tick + RETRY_FOR, warp.at = tick, warp.state = W_ASKED; + } else { + status_set("Link fell through the ground at %s, %s, and the area has no other start position left to try: " + "load the save.", warp.area, warp.pos); + warp.state = W_IDLE; + } + } else if (tick >= warp.at) { + warp.state = W_IDLE; + } } } @@ -644,8 +769,24 @@ static struct { double pos[3], reach; // the game thread's: where it is, and the game's distance to its target float yaw, pitch; uint64_t last_ns, updates; + volatile uint32_t ground_bits; // dbg_freecam_ground: the closest distance seen in the lower view (a float's bits) + volatile uint64_t ground_ns; // and when; 0 none } fc = {.sticks = 1, .speed = 8.0f, .reach = 5.0}; +int dbg_freecam_flying(void) { return fc.on && fc.sticks && !fc.capture; } + +// The replay's thread (vk_dlaa.c fcam_probe): view distances read back from the lower part of the view a few frames +// ago, 0 where it saw the sky. The closest is kept for dbg_pad. +void dbg_freecam_ground(const float *dist, int n) { + float best = 0; + for (int i = 0; i < n; i++) + if (dist[i] > 0 && isfinite(dist[i]) && (best == 0 || dist[i] < best)) best = dist[i]; + uint32_t bits; + memcpy(&bits, &best, 4); + __atomic_store_n(&fc.ground_bits, bits, __ATOMIC_RELAXED); + __atomic_store_n(&fc.ground_ns, plat_now_ns(), __ATOMIC_RELEASE); +} + static double reg_d(Cpu *c, int r) { uint64_t b = (uint64_t)c->v[r]; double v; @@ -718,10 +859,97 @@ int dbg_freecam(int *sticks, float *speed, double pos[3]) { return fc.on; } +// Whether s holds the len characters at part, in any case. +static int contains_nocase(const char *s, const char *part, size_t len) { + for (; *s; s++) { + size_t i = 0; + while (i < len && s[i] && tolower((unsigned char)s[i]) == tolower((unsigned char)part[i])) i++; + if (i == len) return 1; + } + return 0; +} + +// ---- the route (test runs: NX2NAVI_DBG_TEST "route" and "routeto"): Link runs through points of the area on his +// own, so a walking test crosses the field instead of pressing into its walls. The left stick is turned from the way +// he moves towards the next point (the game's camera follows behind him, so a stick to the side turns him), with an +// integral that takes out the camera's own angle; a point counts as reached within ROUTE_NEAR metres. When he gains +// less than a metre on it in 2.5 seconds he is stuck (a wall, a fence, a steep slope): he sidesteps for a while, to +// one side and then the other, and after ROUTE_STUCK such tries the route ends. +#define ROUTE_MAX 16 +#define ROUTE_NEAR 4.0 +#define ROUTE_STUCK 10 +static struct { + volatile int on; + int n, at, have_last, have_heading, side, stuck; + float pt[ROUTE_MAX][2]; // x, z in metres + char name[ROUTE_MAX][48]; + float last[3], best; + double heading, integ; // radians: the way he moved last; the integral of the turn + uint64_t prev_ns, prog_ns, side_until; +} route; + +static void route_start(void) { + route.at = 0, route.have_last = route.have_heading = 0, route.stuck = 0, route.side = 1, route.integ = 0; + route.best = 1e9f, route.prog_ns = plat_now_ns(), route.side_until = 0, route.prev_ns = 0; + route.on = route.n > 0; + if (route.on) rt_log("debug: route of %d points, first %s at %.1f %.1f", route.n, route.name[0], route.pt[0][0], route.pt[0][1]); +} + +// The game's thread: the left stick while a route runs. Stick right turns Link right, which lowers atan2(x, z) (the +// free camera's own frame: ahead (sin yaw, cos yaw), right (-cos yaw, sin yaw)). +static void route_pad(int32_t *lx, int32_t *ly) { + float p[3]; + if (!route.on || !dbg_player_pos(p)) return; + uint64_t now = plat_now_ns(); + double dt = route.prev_ns ? (double)(now - route.prev_ns) / 1e9 : 0; + route.prev_ns = now; + if (dt > 0.1) dt = 0.1; + double dx = route.pt[route.at][0] - p[0], dz = route.pt[route.at][1] - p[2], d = sqrt(dx * dx + dz * dz); + if (d < ROUTE_NEAR) { + rt_log("debug: route: reached %s (%d of %d) at %.1f %.1f %.1f", route.name[route.at], route.at + 1, route.n, p[0], p[1], p[2]); + if (++route.at == route.n) { + route.on = 0; + rt_log("debug: route done"); + return; + } + route.best = 1e9f, route.prog_ns = now, route.integ = 0; + dx = route.pt[route.at][0] - p[0], dz = route.pt[route.at][1] - p[2], d = sqrt(dx * dx + dz * dz); + } + if (route.have_last) { + double mx = p[0] - route.last[0], mz = p[2] - route.last[2]; + if (mx * mx + mz * mz > 0.0025) route.heading = atan2(mx, mz), route.have_heading = 1; + } + memcpy(route.last, p, sizeof route.last), route.have_last = 1; + double err = route.have_heading ? atan2(dx, dz) - route.heading : 0; + while (err > M_PI) err -= 2 * M_PI; + while (err < -M_PI) err += 2 * M_PI; + route.integ += err * dt * 0.4; + route.integ = route.integ > 1.0 ? 1.0 : route.integ < -1.0 ? -1.0 : route.integ; + double a = -(1.2 * err + route.integ); // the stick's angle from straight ahead, right positive + a = a > 1.4 ? 1.4 : a < -1.4 ? -1.4 : a; + if (d < route.best - 1.0f) route.best = (float)d, route.prog_ns = now; + else if (now - route.prog_ns > 2500000000ull && now >= route.side_until) { + if (++route.stuck > ROUTE_STUCK) { + route.on = 0; + rt_log("debug: route given up before %s: stuck %d times at %.1f %.1f %.1f", route.name[route.at], ROUTE_STUCK, p[0], p[1], p[2]); + return; + } + route.side = -route.side; + route.side_until = now + 1500000000ull, route.prog_ns = now + 1500000000ull, route.best = (float)d; + rt_log("debug: route: stuck at %.1f %.1f %.1f, %.1f m before %s: a sidestep to the %s", p[0], p[1], p[2], d, + route.name[route.at], route.side > 0 ? "right" : "left"); + } + if (now < route.side_until) a = route.side * 1.5; + *lx = (int32_t)(sin(a) * 30000), *ly = (int32_t)(cos(a) * 30000); +} + // The game's thread, with the pad's state before the game gets it: while the free camera has the sticks, the left one -// moves it along its view and sideways, the right one turns it, ZR and ZL raise and lower it, R makes it four times -// as fast and L four times as slow, and the game is told that none of those is touched, so Link stays where he is. +// moves it level, ahead where it faces and sideways, the right one turns it, ZR and ZL raise and lower it, R makes it +// four times as fast and L four times as slow, and the game is told that none of those is touched, so Link stays where +// he is. Its height changes only with ZR and ZL: moved along its view, a camera looking down at the ground flew into +// it and showed the void below the map, which looks like ground the game failed to draw. void dbg_pad(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) { + route_pad(lx, ly); if (!fc.on || !fc.sticks || fc.capture) { fc.last_ns = 0; return; @@ -738,9 +966,23 @@ void dbg_pad(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t * fc.yaw -= (float)(in[2] * 2.4 * dt); fc.pitch += (float)(in[3] * 1.8 * dt); fc.pitch = fc.pitch > 1.5f ? 1.5f : fc.pitch < -1.5f ? -1.5f : fc.pitch; - double cp = cos(fc.pitch), dir[3] = {cp * sin(fc.yaw), sin(fc.pitch), cp * cos(fc.yaw)}; + double ahead[3] = {sin(fc.yaw), 0, cos(fc.yaw)}; // where it faces, level double right[3] = {-cos(fc.yaw), 0, sin(fc.yaw)}; // the view's direction crossed with up - for (int i = 0; i < 3; i++) fc.pos[i] += (dir[i] * in[1] + right[i] * in[0]) * step; + for (int i = 0; i < 3; i++) fc.pos[i] += (ahead[i] * in[1] + right[i] * in[0]) * step; + // Clear of the ground: where the closest surface in the lower part of the view (vk_dlaa.c fcam_probe, with DLSS, + // DLAA or FSR 3.1/4 on) is nearer than the clearance, the camera rises instead of going on into a hill and under + // the map, and ZL does not take it lower. The clearance grows with the speed: the distances are a few frames old. + uint64_t seen = __atomic_load_n(&fc.ground_ns, __ATOMIC_ACQUIRE); + if (seen && now - seen < 300000000ull && dt > 0) { + uint32_t bits = __atomic_load_n(&fc.ground_bits, __ATOMIC_RELAXED); + float near_m; + memcpy(&near_m, &bits, 4); + double clear = 2.5 + 0.15 * step / dt; + if (near_m > 0 && near_m < clear) { + if (rise < 0) rise = 0; + fc.pos[1] += (clear - near_m) * (dt * 8 < 1 ? dt * 8 : 1); + } + } fc.pos[1] += rise * step; *lx = *ly = *rx = *ry = 0; *buttons &= ~(zl | zr | l | r); @@ -759,6 +1001,8 @@ static void test_script(uint64_t tick) { const char *e = cfg_get("NX2NAVI_DBG_TEST"); state = e && *e ? 1 : -1; if (state > 0) snprintf(buf, sizeof buf, "%s", e), next = buf; + // the places and events, read with the flag names as the menu starts them, for warp, event and routeto + if (state > 0) dbg_flags_refresh(), dbg_names(NULL); } while (state > 0 && next && *next && strtoull(next, NULL, 10) <= tick) { const char *colon = strchr(next, ':'), *end = strchr(next, ';'); @@ -787,6 +1031,45 @@ static void test_script(uint64_t tick) { for (int i = 0; i < 3; i++) fc.pos[i] += d[i]; fc.yaw += yaw, fc.pitch += pitch; } + } else if (!strncmp(colon + 1, "route ", 6)) { // route [ ...]: points in metres + const char *q = colon + 7; + double x, z; + int used = 0; + route.n = 0; + while (route.n < ROUTE_MAX && sscanf(q, "%lf %lf%n", &x, &z, &used) == 2 && (!end || q + used <= end)) { + route.pt[route.n][0] = (float)x, route.pt[route.n][1] = (float)z; + snprintf(route.name[route.n], sizeof route.name[0], "point %d", route.n + 1); + route.n++, q += used; + } + route_start(); + } else if (!strncmp(colon + 1, "routeto ", 8)) { + // routeto [/] ...: the area's own start positions, in order + char area[64] = "", item[128]; + text_get("Sequence_CurrentBanc", area); + const char *q = colon + 9; + int used = 0; + route.n = 0; + while (route.n < ROUTE_MAX && sscanf(q, "%127[^ ;]%n", item, &used) == 1 && (!end || q + used <= end)) { + q += used; + while (*q == ' ') q++; + int found = -1; + for (int i = 0; found < 0 && i < dbg_places(); i++) { + if (strcmp(places[i].area, area) || !places[i].has_xyz) continue; + for (const char *tok = item; *tok && found < 0;) { // the parts between the slashes + size_t len = strcspn(tok, "/"); + if (len && contains_nocase(places[i].pos, tok, len)) found = i; + tok += len + (tok[len] == '/'); + } + } + if (found < 0) { + rt_log("debug: routeto: no start position of %s with coordinates matches \"%s\"", area, item); + continue; + } + route.pt[route.n][0] = places[found].xyz[0], route.pt[route.n][1] = places[found].xyz[2]; + snprintf(route.name[route.n], sizeof route.name[0], "%s", places[found].pos); + route.n++; + } + route_start(); } else if (!strncmp(colon + 1, "warp ", 5)) { char area[64] = "", pos[64] = ""; if (sscanf(colon + 6, "%63[^ ;] %63[^ ;]", area, pos) >= 1) dbg_warp(area, pos); diff --git a/runtime/debug_tools.h b/runtime/debug_tools.h index 5f76f315..21286183 100644 --- a/runtime/debug_tools.h +++ b/runtime/debug_tools.h @@ -34,7 +34,7 @@ int dbg_names(int *found); // 0 not started, 1 being read from the g // time: nothing is taken while dbg_busy. dbg_status is the last thing that happened, in words. int dbg_places(void); const char *dbg_place_area(int i); -const char *dbg_place_pos(int i); // "": the area's default start +const char *dbg_place_pos(int i); // never "": a teleport without a listed position takes the area's first void dbg_warp(const char *area, const char *pos); int dbg_events(void); const char *dbg_event_name(int i); @@ -48,6 +48,8 @@ int dbg_freecam_available(void); // the build has the camera's patch set void dbg_freecam_set(int on, int sticks, float speed); void dbg_freecam_reset(void); // back to the game's view, free again from there int dbg_freecam(int *sticks, float *speed, double pos[3]); // on or off +int dbg_freecam_flying(void); // on, with the sticks: vk_dlaa.c reads the ground's distance back for it +void dbg_freecam_ground(const float *dist, int n); // the replay's thread: view distances in the lower view, 0 sky // Memory: values of 1, 2 or 4 bytes in the game's heap. int dbg_search_new(uint32_t value, int size); // returns the number of places diff --git a/runtime/ui/debug_menu.cpp b/runtime/ui/debug_menu.cpp index 558c2646..956604c0 100644 --- a/runtime/ui/debug_menu.cpp +++ b/runtime/ui/debug_menu.cpp @@ -309,9 +309,12 @@ void tab_world(void) { ImGui::TextDisabled("x %.1f y %.1f z %.1f", at[0], at[1], at[2]); } if (changed) dbg_freecam_set(on, sticks, cam_speed); - ImGui::TextWrapped("Close this menu to fly: the left stick moves along the view and sideways, the right stick turns, ZR and ZL " - "go up and down, R is four times as fast and L four times as slow. The game goes on meanwhile and " - "draws what the camera sees; the game speed above slows it down. The camera does not stop at walls."); + ImGui::TextWrapped("Close this menu to fly: the left stick moves level, ahead and sideways, the right stick turns, ZR and ZL " + "go up and down, R is four times as fast and L four times as slow. The height changes only with ZR " + "and ZL, and with DLSS, DLAA or FSR 3.1 or 4 on the camera rises where the ground ahead or below comes " + "within a few metres. The game goes on meanwhile and draws what the camera sees; the game speed above " + "slows it down. Without those upscalers it does not stop at hills: below the ground the picture shows " + "the void under the map, not a missing floor."); } // ---- memory diff --git a/runtime/vk_dlaa.c b/runtime/vk_dlaa.c index c38fbbe8..7f5f51db 100644 --- a/runtime/vk_dlaa.c +++ b/runtime/vk_dlaa.c @@ -1345,6 +1345,55 @@ static void hl_destroy(void) { // The device is idle here. Release NGX features and parameters before their Vulkan resources, then shut down NGX even // if initialization succeeded but the layout check failed. The Linux core's Shutdown1 takes an extra output pointer // compared with the SDK's public wrapper; it reports the number of devices still using NGX. +// ---- the free camera's view of the ground (debug_tools.c dbg_pad). While it flies, nine of the view distances +// mv_pass wrote (a grid over the lower part of the view, where the ground is) go back to the host, and the camera rises +// where the closest is nearer than its clearance: flown through a hill, it showed the void below the map. A slot is +// read FC_SLOTS evaluations after its copy, when the GPU has long been past it. +int dbg_freecam_flying(void); // debug_tools.c +void dbg_freecam_ground(const float *dist, int n); +#define FC_N 9 +#define FC_SLOTS 4 +static VkBuffer fc_buf; +static VmaAllocation fc_mem; +static float *fc_map; +static uint32_t fc_next; +static uint8_t fc_have[FC_SLOTS]; + +static void fcam_probe(VkCommandBuffer cb, const struct Size *e, uint32_t w, uint32_t h) { + if (!dbg_freecam_flying() || !e->dist_ok) { // dist_ok: mv_pass wrote this frame's distances (Ocarina's frame) + memset(fc_have, 0, sizeof fc_have); + return; + } + const VkCtx *c = vk_ctx(); + if (!fc_buf) { + VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = FC_SLOTS * FC_N * sizeof(float), + .usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT}; + VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST, + .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT}; + VmaAllocationInfo info; + if (vmaCreateBuffer(c->vma, &bi, &ai, &fc_buf, &fc_mem, &info) != VK_SUCCESS) { + fc_buf = VK_NULL_HANDLE; + return; + } + fc_map = info.pMappedData; + } + uint32_t s = fc_next++ % FC_SLOTS; + if (fc_have[s]) { + vmaInvalidateAllocation(c->vma, fc_mem, s * FC_N * sizeof(float), FC_N * sizeof(float)); + dbg_freecam_ground(fc_map + s * FC_N, FC_N); + } + // the replay's viewports have negative heights: row 0 is the top of the view + static const float fx[3] = {0.2f, 0.5f, 0.8f}, fy[3] = {0.6f, 0.78f, 0.96f}; + VkBufferImageCopy r[FC_N]; + for (int i = 0; i < FC_N; i++) + r[i] = (VkBufferImageCopy){.bufferOffset = (s * FC_N + i) * sizeof(float), + .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .imageOffset = {(int32_t)(fx[i % 3] * (w - 1)), (int32_t)(fy[i / 3] * (h - 1)), 0}, + .imageExtent = {1, 1, 1}}; + vkCmdCopyImageToBuffer(cb, e->dist[e->dist_cur ^ 1], VK_IMAGE_LAYOUT_GENERAL, fc_buf, FC_N, r); + fc_have[s] = 1; +} + void vk_dlaa_release(void) { #ifndef _WIN32 const VkCtx *c = vk_ctx(); @@ -1365,6 +1414,7 @@ void vk_dlaa_release(void) { if (D.mv_samp) vkDestroySampler(c->device, D.mv_samp, NULL); if (D.camb) vmaDestroyBuffer(c->vma, D.camb, D.camb_mem); if (D.mvm) vmaDestroyBuffer(c->vma, D.mvm, D.mvm_mem); // mapped for good: the destroy unmaps it + if (fc_buf) vmaDestroyBuffer(c->vma, fc_buf, fc_mem), fc_buf = VK_NULL_HANDLE, fc_map = NULL; if (R.pipe) vkDestroyPipeline(c->device, R.pipe, NULL); if (R.layout) vkDestroyPipelineLayout(c->device, R.layout, NULL); if (R.dsl) vkDestroyDescriptorSetLayout(c->device, R.dsl, NULL); @@ -1666,6 +1716,7 @@ static void evaluate(VkExec *ex, const VkExecTarget *dst) { uint64_t c0 = timed ? kern_now_ns() : 0; rec_drain(); // NGX records into the command buffer itself: the recorder thread must have made every call so far if (timed) T.cpu_drain += kern_now_ns() - c0; + if (mv == 1) fcam_probe(cb, e, w, h); // mv_pass's distances, behind its barrier if (fsr) { // FSR 3.1 (vk_ffx.cpp) in DLSS's place: the same images, then the same blit below if (t->level || t->base_layer || z->level || z->base_layer) return dl_fail("FSR 3.1 on a target that is not level 0, layer 0", 0); if (D.last_color != t->image) target_changed(t, mv == 1);