From e6d6ecef805c1f050ff04baef00c4029e1e98011 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 25 Sep 2026 10:48:20 +0200 Subject: [PATCH] doom: fix reboot, unreachable L1 demon and no exit; get mem: don't lock IRQs - doom: each turn shrank the view vectors (cos = 1 - ROT/8, scale ~0.993, plus fixed-point truncation), so the level seemed to grow and after ~10 turns a zero wall height was divided by: usage fault, reboot. The view is renormalised to unit length after every turn; the ray step and wall height are clamped - doom: the L1 demon spawned inside a wall tile, so it could not be shot and the portal never opened; it now mirrors the imp at (11,4) - doom: the 60 s UI lock engaged mid-game, so the exit tap went to the lock screen; the Doom tick now keeps the lock and auto-off timers alive. The user button no longer also fires (joystick press does) - doom: sprites keep a brightness floor at distance (L2 boss is still hard to see; open) - get mem (MEM_STATS builds): walk threads with k_thread_foreach_unlocked; the stack scan under the thread-list spinlock held IRQs off long enough to trip the BLE controller's radio-ISR latency assert --- zephcore/helpers/CommonCLI.cpp | 7 +- .../helpers/ui-joystick/joystick_ui_task.cpp | 7 ++ zephcore/helpers/ui/doom_game.c | 100 +++++++++++++----- 3 files changed, 89 insertions(+), 25 deletions(-) diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 3fec6e1..1e98fab 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -202,7 +202,12 @@ static void memStatsLine(int n, char *reply) struct mem_stats_walk w = { n, 0, reply }; reply[0] = '\0'; - k_thread_foreach(memStatsThread, &w); + /* Unlocked: the callback scans a whole stack for its high-water mark, + * and k_thread_foreach() holds the thread-list spinlock (interrupts off) + * across it -- a few hundred us on nRF52, past the BLE controller's + * 150 us inter-frame deadline: an advertising event asserted + * (lll_adv.c "Radio ISR latency") and rebooted the node. */ + k_thread_foreach_unlocked(memStatsThread, &w); if (reply[0] != '\0') { return; } diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index adadd61..358c84a 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -929,6 +929,13 @@ void JoystickUITask::loop() #ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM if (doom_game_is_running()) { + /* The game takes the joystick straight from the input callback, so + * no key reaches the paths that normally keep these alive: the UI + * locked itself mid-game and the exit key then went to the lock + * screen (and an auto-off swallowed it as a wake press). Playing is + * activity. */ + scheduleLockTimer(); + mc_display_reset_auto_off(); _next_refresh = now + 500; if (s_schedule_render_fn) s_schedule_render_fn(500); return; diff --git a/zephcore/helpers/ui/doom_game.c b/zephcore/helpers/ui/doom_game.c index 4a82969..1042c49 100644 --- a/zephcore/helpers/ui/doom_game.c +++ b/zephcore/helpers/ui/doom_game.c @@ -374,6 +374,54 @@ static void doom_map_init(void) : &level_1[0][0]; } +static uint32_t isqrt64(uint64_t v) +{ + uint64_t r = 0; + uint64_t bit = 1ULL << 62; + + while (bit > v) { + bit >>= 2; + } + while (bit) { + if (v >= r + bit) { + v -= r + bit; + r = (r >> 1) + bit; + } else { + r >>= 1; + } + bit >>= 2; + } + return (uint32_t)r; +} + +/* Turn by sin_r (signed) and put the view back on the unit circle. + * + * The rotation used to be cos = 1 - ROT/8, sin = ROT: a matrix with scale + * ~0.993, plus truncation in every fp_mul, so each turn tick shrank dir and + * plane. Half a view vector after ~1.3 full turns, 1/16 after ~5: walls read + * as ever farther away (the level "stretches for kilometres", steps get + * short), and around ten turns 1/ray_dir overflowed int32 in the raycaster + * and a zero wall height divided by zero - a usage fault that rebooted the + * node. Now: the small-angle cosine, then dir renormalised to exactly unit + * length and plane rebuilt perpendicular to it at its 2/3 field of view. */ +static void rotate_view(struct doom_player *p, fixed_t sin_r) +{ + fixed_t cos_r = FP_ONE - fp_mul(sin_r, sin_r) / 2; + fixed_t dx = fp_mul(p->dir_x, cos_r) - fp_mul(p->dir_y, sin_r); + fixed_t dy = fp_mul(p->dir_x, sin_r) + fp_mul(p->dir_y, cos_r); + uint32_t len = isqrt64((uint64_t)((int64_t)dx * dx) + (uint64_t)((int64_t)dy * dy)); + + if (len == 0) { + dx = 0; + dy = -FP_ONE; + len = FP_ONE; + } + p->dir_x = (fixed_t)(((int64_t)dx << FP_SHIFT) / len); + p->dir_y = (fixed_t)(((int64_t)dy << FP_SHIFT) / len); + p->plane_x = -(p->dir_y * 2) / 3; + p->plane_y = (p->dir_x * 2) / 3; +} + static uint8_t doom_map_get(int x, int y) { if (x < 0 || x >= MAP_W || y < 0 || y >= MAP_H) { @@ -648,7 +696,14 @@ static void render_one_sprite(const uint16_t *sprite, int draw_start_x = sprite_screen_x - sprite_h / 2; int draw_end_x = sprite_screen_x + sprite_h / 2; + /* Floor for sprites: at 8+ tiles the wall fade (brightness 1) passes one + * dither cell in 16, so the boss, which starts ~11 tiles away in a + * corner, drew as a few dots or nothing while its fireballs hit. Walls + * keep the full fade; sprites keep a silhouette. */ int brightness = dist_to_brightness(transform_y); + if (brightness < 7) { + brightness = 7; + } for (int sc = draw_start_x; sc < draw_end_x; sc++) { if (sc < 0 || sc >= SCREEN_W) continue; @@ -756,11 +811,13 @@ static void raycaster_render(void) int map_x = fp_to_int(p->x); int map_y = fp_to_int(p->y); - fixed_t delta_dist_x = (ray_dir_x == 0) - ? INT32_MAX + /* 1/|ray_dir|, capped: near an axis it exceeds int32 and the DDA + * below adds it up. 1024 tiles is far past any ray in a 16x16 map. */ + fixed_t delta_dist_x = (fp_abs(ray_dir_x) < (FP_ONE >> 10)) + ? fp_from_int(1024) : fp_abs(fp_div(FP_ONE, ray_dir_x)); - fixed_t delta_dist_y = (ray_dir_y == 0) - ? INT32_MAX + fixed_t delta_dist_y = (fp_abs(ray_dir_y) < (FP_ONE >> 10)) + ? fp_from_int(1024) : fp_abs(fp_div(FP_ONE, ray_dir_y)); int step_x, step_y; @@ -839,6 +896,12 @@ static void raycaster_render(void) int line_height = fp_to_int(fp_div(fp_from_int(SCREEN_H), perp_dist)); + /* A wall farther than SCREEN_H rounds to 0 lines, and the texture + * row below divides by this: a usage fault (divide by zero) that + * rebooted the node mid-game. */ + if (line_height < 1) { + line_height = 1; + } int draw_start = (SCREEN_H - line_height) / 2; int draw_end = draw_start + line_height; @@ -1192,10 +1255,13 @@ static void game_init(void) .y = fp_from_int(4) + FP_HALF, .health = 30, }; + /* x = 11, mirroring the imp: (12,4) is inside the NE wall block, where + * line of sight never reached the demon, so it could not be shot and the + * portal never opened. */ game.enemies[1] = (struct doom_enemy){ .type = ENEMY_DEMON, .state = ESTATE_IDLE, - .x = fp_from_int(12) + FP_HALF, + .x = fp_from_int(11) + FP_HALF, .y = fp_from_int(4) + FP_HALF, .health = 60, }; @@ -1247,27 +1313,11 @@ static void handle_movement(uint32_t input) } if (input & DINPUT_LEFT) { - fixed_t old_dx = p->dir_x; - fixed_t old_px = p->plane_x; - fixed_t cos_r = FP_ONE - (ROT_SPEED / 8); - fixed_t sin_r = ROT_SPEED; - - p->dir_x = fp_mul(old_dx, cos_r) - fp_mul(p->dir_y, sin_r); - p->dir_y = fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r); - p->plane_x = fp_mul(old_px, cos_r) - fp_mul(p->plane_y, sin_r); - p->plane_y = fp_mul(old_px, sin_r) + fp_mul(p->plane_y, cos_r); + rotate_view(p, ROT_SPEED); } if (input & DINPUT_RIGHT) { - fixed_t old_dx = p->dir_x; - fixed_t old_px = p->plane_x; - fixed_t cos_r = FP_ONE - (ROT_SPEED / 8); - fixed_t sin_r = ROT_SPEED; - - p->dir_x = fp_mul(old_dx, cos_r) + fp_mul(p->dir_y, sin_r); - p->dir_y = -fp_mul(old_dx, sin_r) + fp_mul(p->dir_y, cos_r); - p->plane_x = fp_mul(old_px, cos_r) + fp_mul(p->plane_y, sin_r); - p->plane_y = -fp_mul(old_px, sin_r) + fp_mul(p->plane_y, cos_r); + rotate_view(p, -ROT_SPEED); } } @@ -1779,7 +1829,9 @@ void doom_game_input(uint16_t code, int32_t value) bit = DINPUT_LEFT; break; case INPUT_KEY_ENTER: - case INPUT_KEY_0: + /* Not INPUT_KEY_0: on the Wio that is the user button, whose tap is + * the way out (KEY_1 via the multi-tap filter); firing on it too + * shot on every exit. */ bit = DINPUT_FIRE; break; default: