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);