From ea0e07acbe60f5ffbf123080d0fdf9db2b473be4 Mon Sep 17 00:00:00 2001 From: Claude Worker claude2 Date: Thu, 11 Jun 2026 04:45:56 +0200 Subject: [PATCH] feat(intro): port 3D FX phases B/C/D Rotozoom, dot sphere et ray corridor portes depuis le FxEngine Arduino (boucles de rendu seules, sans CapsAllocator ni timelines). Rendu 240x160 RGB565 en PSRAM double en pixels vers un lv_canvas plein ecran derriere logo+scroller ; phases A 7s puis 3x3s ; fallback starfield si l'alloc PSRAM echoue. Build vert (35% libre). --- .../components/display_ui/CMakeLists.txt | 1 + .../components/display_ui/display_ui.cpp | 61 +++- .../display_ui/include/intro_fx3d.h | 33 ++ .../components/display_ui/intro_fx3d.cpp | 334 ++++++++++++++++++ 4 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 idf_zacus/components/display_ui/include/intro_fx3d.h create mode 100644 idf_zacus/components/display_ui/intro_fx3d.cpp diff --git a/idf_zacus/components/display_ui/CMakeLists.txt b/idf_zacus/components/display_ui/CMakeLists.txt index 8fec8d1..e2bd212 100644 --- a/idf_zacus/components/display_ui/CMakeLists.txt +++ b/idf_zacus/components/display_ui/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register( SRCS "display_ui.cpp" + "intro_fx3d.cpp" "buttons_input.c" "fonts/lv_font_orbitron_40.c" "fonts/lv_font_ibmplexmono_18.c" diff --git a/idf_zacus/components/display_ui/display_ui.cpp b/idf_zacus/components/display_ui/display_ui.cpp index 43bb86f..50bea49 100644 --- a/idf_zacus/components/display_ui/display_ui.cpp +++ b/idf_zacus/components/display_ui/display_ui.cpp @@ -250,15 +250,20 @@ static volatile uint8_t s_browser_sel = 0; static volatile bool s_browser_open = false; static volatile bool s_browser_reload = false; -// ─── Intro — faithful port of the original cracktro phase A ────────────────── -// Algorithms from ui_manager_intro.cpp: 3-layer parallax starfield in Q8.8 +// ─── Intro — faithful port of the original cracktro ────────────────────────── +// Phase A from ui_manager_intro.cpp: 3-layer parallax starfield in Q8.8 // fixed point (layer split 50/30/20%, speeds {54,116,198} px/s, sizes 1-3 px, // opacities 30/60/100%, 1-in-8 twinkle), logo + drop shadow (offset 1,23), -// bottom sine-wave scrolltext, copper bars. The 3D FX modes of phases B/C -// (dot sphere, ray corridor, rotozoom) are NOT ported yet. +// bottom sine-wave scrolltext, copper bars. Phases B/C/D are the original 3D +// FX (rotozoom, dot sphere, ray corridor) ported in intro_fx3d.cpp, rendered +// into a full-screen LVGL canvas behind the logo + scroller. If the canvas +// PSRAM allocation fails, the intro gracefully stays on phase A throughout. +#include "intro_fx3d.h" #define INTRO_STARS 48 #define INTRO_BARS 4 -#define INTRO_DURATION_MS 8000 +#define INTRO_PHASE_A_MS 7000 +#define INTRO_FX_MS 3000 +#define INTRO_DURATION_MS (INTRO_PHASE_A_MS + 3 * INTRO_FX_MS) typedef struct { int32_t x_q8, y_q8; uint8_t layer; int16_t speed; } intro_star_t; static lv_obj_t *s_scr_intro; static lv_obj_t *s_intro_logo, *s_intro_logo_shadow; @@ -269,6 +274,9 @@ static lv_obj_t *s_intro_bars[INTRO_BARS]; static int32_t s_scroll_x_q8; // scrolltext x in Q8.8 static uint32_t s_intro_t_ms; // elapsed intro time static volatile bool s_intro_done = false; +static lv_obj_t *s_fx_canvas; // phases B/C/D render target +static lv_color_t *s_fx_canvas_buf; // 480x320 RGB565, PSRAM +static bool s_fx_tried, s_fx_ok; // lazy one-shot setup at phase B // Original kIntro defaults: scroll 90 px/s, sine amp 14 px, period 120 px. #define INTRO_SCROLL_PXS 90 #define INTRO_SINE_AMP 14 @@ -660,10 +668,52 @@ static void build_intro_screen(void) { lv_obj_set_pos(s_intro_scroll, DUI_HOR_RES, DUI_VER_RES - 60); } +// Phase B entry: allocate the FX module + a full-screen canvas behind the +// logo/scroller, hide the phase-A starfield + bars. One-shot; on failure the +// intro simply keeps the starfield (s_fx_ok stays false). Display task only. +static bool intro_fx_setup(void) { + if (!fx3d_init()) return false; + s_fx_canvas_buf = (lv_color_t *) heap_caps_malloc( + (size_t) DUI_HOR_RES * DUI_VER_RES * sizeof(lv_color_t), + MALLOC_CAP_SPIRAM); + if (!s_fx_canvas_buf) return false; + s_fx_canvas = lv_canvas_create(s_scr_intro); + lv_canvas_set_buffer(s_fx_canvas, s_fx_canvas_buf, + DUI_HOR_RES, DUI_VER_RES, LV_IMG_CF_TRUE_COLOR); + lv_obj_set_pos(s_fx_canvas, 0, 0); + lv_obj_move_background(s_fx_canvas); + for (int i = 0; i < INTRO_STARS; i++) + lv_obj_add_flag(s_star_objs[i], LV_OBJ_FLAG_HIDDEN); + for (int i = 0; i < INTRO_BARS; i++) + lv_obj_add_flag(s_intro_bars[i], LV_OBJ_FLAG_HIDDEN); + return true; +} + // Per-tick intro animation (display task only; dt in ms). Faithful Q8.8 math. static void update_intro(uint32_t dt_ms) { s_intro_t_ms += dt_ms; + // Phase sequencing: A starfield, then B/C/D = rotozoom, dot sphere, + // ray corridor (original fx_mode order) on the FX canvas. + if (s_intro_t_ms >= INTRO_PHASE_A_MS && !s_fx_tried) { + s_fx_tried = true; + s_fx_ok = intro_fx_setup(); + } + if (s_fx_ok && s_intro_t_ms >= INTRO_PHASE_A_MS) { + int fx = (int) ((s_intro_t_ms - INTRO_PHASE_A_MS) / INTRO_FX_MS); + if (fx > 2) fx = 2; + // Render every other tick (20 ms): the SPI flush of a full-screen + // canvas dominates anyway, no point racing it. + static bool skip; + skip = !skip; + if (!skip) { + fx3d_render((fx3d_mode_t) fx, s_intro_t_ms, + (uint16_t *) s_fx_canvas_buf, DUI_HOR_RES, DUI_VER_RES); + lv_obj_invalidate(s_fx_canvas); + } + goto scroller; // stars/bars hidden — skip their updates + } + // Starfield: fall + wrap + twinkle (original update loop). for (int i = 0; i < INTRO_STARS; i++) { intro_star_t *st = &s_stars[i]; @@ -685,6 +735,7 @@ static void update_intro(uint32_t dt_ms) { lv_obj_set_y(s_intro_bars[i], 60 + i * 14 + (int) (sinf(ph) * 18.0f)); } +scroller: // Sine scroller: leftward at INTRO_SCROLL_PXS, y rides a sine of the // x position (original sine_amp/sine_period defaults). s_scroll_x_q8 -= (int32_t) ((INTRO_SCROLL_PXS * dt_ms * 256u) / 1000u); diff --git a/idf_zacus/components/display_ui/include/intro_fx3d.h b/idf_zacus/components/display_ui/include/intro_fx3d.h new file mode 100644 index 0000000..d36c57b --- /dev/null +++ b/idf_zacus/components/display_ui/include/intro_fx3d.h @@ -0,0 +1,33 @@ +// Intro cracktro — 3D FX phases B/C/D (rotozoom, dot sphere, ray corridor). +// Faithful per-pixel ports of ui_freenove_allinone/src/ui/fx/fx_engine.cpp +// (renderMidRotoZoom, renderDotSphere3D, renderRayCorridor) rendered at +// 240x160 in a PSRAM buffer then pixel-doubled to the 480x320 LVGL canvas. +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define FX3D_W 240 +#define FX3D_H 160 + +typedef enum { + FX3D_ROTOZOOM = 0, + FX3D_DOTSPHERE = 1, + FX3D_CORRIDOR = 2, +} fx3d_mode_t; + +// Allocate the low-res buffer + LUTs/textures (PSRAM). Idempotent. +bool fx3d_init(void); + +// Render `mode` at time t_ms into dst (RGB565, dst_w x dst_h) with 2x pixel +// doubling. dst must be exactly FX3D_W*2 x FX3D_H*2; anything else is a no-op. +void fx3d_render(fx3d_mode_t mode, uint32_t t_ms, uint16_t *dst, + int dst_w, int dst_h); + +#ifdef __cplusplus +} +#endif diff --git a/idf_zacus/components/display_ui/intro_fx3d.cpp b/idf_zacus/components/display_ui/intro_fx3d.cpp new file mode 100644 index 0000000..c156dd2 --- /dev/null +++ b/idf_zacus/components/display_ui/intro_fx3d.cpp @@ -0,0 +1,334 @@ +// Intro cracktro 3D FX — ports of the original Arduino FxEngine renderers +// (ui_freenove_allinone/src/ui/fx/fx_engine.cpp): renderMidRotoZoom, +// renderDotSphere3D (+ kDotSphere3D init), renderRayCorridor (+ kRayCorridor +// init). Only the render loops are ported — no CapsAllocator, timelines or +// DMA line buffers. Everything renders into a 240x160 RGB565 low-res buffer +// (PSRAM) then gets pixel-doubled into the caller's 480x320 canvas. +#include "intro_fx3d.h" + +#include +#include + +#include "esp_heap_caps.h" + +namespace { + +constexpr int kRotoTexSize = 128; // FxEngine kRotoTexSize +constexpr int kRayTexSize = 64; // FxEngine kRayTexSize +constexpr int kDotCount = 360; // ~W*H/75 clamped (original formula) +constexpr int kDotRadius = 72; // min_dim/2 - 8 clamped to [24,72] +constexpr int kDotBlobR = 2; + +struct DotPt { int16_t x, y, z; }; + +uint16_t *s_lowres; // FX3D_W * FX3D_H +uint16_t *s_roto_tex; // 128 * 128 +uint16_t *s_ray_tex; // 64 * 64 +DotPt *s_dots; +uint16_t s_dot_shade[256]; +int8_t s_ray_col_off[FX3D_W]; +uint16_t s_ray_floor_q12[FX3D_H]; +int16_t s_sin_q15[256]; +bool s_ready = false; + +// ---- helpers (FxEngine::rgb565 / mul565_u8 / addSat565 / sin8) ---- + +uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) { + return (uint16_t) (((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3)); +} + +uint16_t mul565_u8(uint16_t c, uint8_t v) { + uint16_t r = (uint16_t) ((c >> 11) & 31u); + uint16_t g = (uint16_t) ((c >> 5) & 63u); + uint16_t b = (uint16_t) (c & 31u); + r = (uint16_t) ((r * v + 128u) >> 8); + g = (uint16_t) ((g * v + 128u) >> 8); + b = (uint16_t) ((b * v + 128u) >> 8); + return (uint16_t) ((r << 11) | (g << 5) | b); +} + +uint16_t add_sat565(uint16_t a, uint16_t b) { + uint16_t ar = (uint16_t) ((a >> 11) & 31u), ag = (uint16_t) ((a >> 5) & 63u), ab = (uint16_t) (a & 31u); + uint16_t br = (uint16_t) ((b >> 11) & 31u), bg = (uint16_t) ((b >> 5) & 63u), bb = (uint16_t) (b & 31u); + const uint16_t rr = (uint16_t) ((ar + br > 31u) ? 31u : (ar + br)); + const uint16_t gg = (uint16_t) ((ag + bg > 63u) ? 63u : (ag + bg)); + const uint16_t b2 = (uint16_t) ((ab + bb > 31u) ? 31u : (ab + bb)); + return (uint16_t) ((rr << 11) | (gg << 5) | b2); +} + +int16_t sin_q15(uint8_t a) { return s_sin_q15[a]; } +int16_t cos_q15(uint8_t a) { return s_sin_q15[(uint8_t) (a + 64u)]; } +// fx_sin8/fx_cos8 equivalents: amplitude -128..127. +int16_t sin8(uint8_t a) { return (int16_t) (s_sin_q15[a] >> 8); } +int16_t cos8(uint8_t a) { return (int16_t) (s_sin_q15[(uint8_t) (a + 64u)] >> 8); } + +template +T clampv(T v, T lo, T hi) { return (v < lo) ? lo : (v > hi) ? hi : v; } + +void fill_lowres(uint16_t color) { + // 32-bit fill (buffer is 4-byte aligned, FX3D_W*FX3D_H even) + const uint32_t packed = (uint32_t) color | ((uint32_t) color << 16); + uint32_t *dst32 = (uint32_t *) s_lowres; + for (size_t i = 0; i < (size_t) FX3D_W * FX3D_H / 2; i++) dst32[i] = packed; +} + +void add_pixel(int x, int y, uint16_t color) { + if (x < 0 || y < 0 || x >= FX3D_W || y >= FX3D_H) return; + const size_t idx = (size_t) y * FX3D_W + (size_t) x; + s_lowres[idx] = add_sat565(s_lowres[idx], color); +} + +void *psram_alloc(size_t bytes) { + void *p = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM); + if (!p) p = heap_caps_malloc(bytes, MALLOC_CAP_DEFAULT); + return p; +} + +// ---- renderers (faithful ports) ---- + +// FxEngine::renderMidRotoZoom — additive rotozoom of the radial-checker +// texture generated in FxEngine::begin(). +void render_rotozoom(uint32_t now_ms) { + fill_lowres(rgb565(4u, 8u, 16u)); + const int cx = FX3D_W / 2, cy = FX3D_H / 2; + const uint8_t phase = (uint8_t) ((now_ms / 10u) & 0xFFu); + const int16_t s = sin8(phase); + const int16_t c = cos8(phase); + const int16_t pulse = (int16_t) (sin8((uint8_t) (phase * 2u)) >> 1); + const int16_t zoom_q8 = (int16_t) (256 + pulse); + + for (int y = 0; y < FX3D_H; y++) { + const int16_t dy = (int16_t) (y - cy); + const size_t row = (size_t) y * FX3D_W; + for (int x = 0; x < FX3D_W; x++) { + const int16_t dx = (int16_t) (x - cx); + int32_t u = (c * dx - s * dy); + int32_t v = (s * dx + c * dy); + u = (u * zoom_q8) >> 8; + v = (v * zoom_q8) >> 8; + const int tx = (int) ((u + kRotoTexSize / 2) & (kRotoTexSize - 1)); + const int ty = (int) ((v + kRotoTexSize / 2) & (kRotoTexSize - 1)); + const uint16_t tex = s_roto_tex[(size_t) ty * kRotoTexSize + (size_t) tx]; + s_lowres[row + x] = add_sat565(s_lowres[row + x], mul565_u8(tex, 180u)); + } + } +} + +// FxEngine::renderDotSphere3D — lit point sphere, Q15 LUT rotations. +void render_dotsphere(uint32_t now_ms) { + fill_lowres(0x0000u); + const uint8_t ax = (uint8_t) (now_ms >> 4); + const uint8_t ay = (uint8_t) (now_ms >> 5); + const uint8_t az = (uint8_t) (now_ms >> 6); + const int16_t cx_r = cos_q15(ax), sx_r = sin_q15(ax); + const int16_t cy_r = cos_q15(ay), sy_r = sin_q15(ay); + const int16_t cz_r = cos_q15(az), sz_r = sin_q15(az); + const int16_t lx = (int16_t) (0.30f * 32767.0f); + const int16_t ly = (int16_t) (-0.20f * 32767.0f); + const int16_t lz = (int16_t) (0.93f * 32767.0f); + const int center_x = FX3D_W / 2, center_y = FX3D_H / 2; + const int fov = (FX3D_W < FX3D_H) ? FX3D_W : FX3D_H; + + for (int i = 0; i < kDotCount; i++) { + const DotPt &dot = s_dots[i]; + const int32_t x = ((int32_t) dot.x * kDotRadius) >> 7; + const int32_t y = ((int32_t) dot.y * kDotRadius) >> 7; + const int32_t z = ((int32_t) dot.z * kDotRadius) >> 7; + + const int32_t y1 = (y * cx_r - z * sx_r) >> 15; + const int32_t z1 = (y * sx_r + z * cx_r) >> 15; + const int32_t x2 = (x * cy_r + z1 * sy_r) >> 15; + const int32_t z2 = (-x * sy_r + z1 * cy_r) >> 15; + const int32_t x3 = (x2 * cz_r - y1 * sz_r) >> 15; + const int32_t y3 = (x2 * sz_r + y1 * cz_r) >> 15; + + const int32_t depth = z2 + (kDotRadius * 3); + if (depth <= 1) continue; + + const int sxp = center_x + (int) ((x3 * fov) / depth); + const int syp = center_y + (int) ((y3 * fov) / depth); + if (sxp < 0 || syp < 0 || sxp >= FX3D_W || syp >= FX3D_H) continue; + + const int32_t nd = (x3 * lx + y3 * ly + z2 * lz) >> 15; + const int32_t ndotl = clampv((nd * 128) / kDotRadius + 128, 0, 255); + const uint16_t base = s_dot_shade[ndotl]; + + for (int yy = -kDotBlobR; yy <= kDotBlobR; yy++) { + for (int xx = -kDotBlobR; xx <= kDotBlobR; xx++) { + const int d2 = xx * xx + yy * yy; + if (d2 > kDotBlobR * kDotBlobR) continue; + const int atten = clampv(255 - d2 * 28, 0, 255); + add_pixel(sxp + xx, syp + yy, mul565_u8(base, (uint8_t) atten)); + } + } + } +} + +// FxEngine::renderRayCorridor — textured tunnel walls + scrolling floor. +void render_corridor(uint32_t now_ms) { + fill_lowres(0x0000u); + const int horizon = FX3D_H / 2; + const uint32_t zscroll = now_ms >> 3; + const uint8_t camera_angle = (uint8_t) (now_ms >> 6); + + for (int x = 0; x < FX3D_W; x++) { + const int8_t off = s_ray_col_off[x]; + const uint8_t ray_angle = (uint8_t) (camera_angle + (uint8_t) off); + const int16_t dir_x = sin_q15(ray_angle); + const int16_t dir_z = cos_q15(ray_angle); + const int16_t abs_dir_x = (int16_t) ((dir_x < 0) ? -dir_x : dir_x); + if (abs_dir_x < 64) { + for (int y = horizon; y < FX3D_H; y++) { + const int dy = y - horizon; + const uint8_t shade = (uint8_t) (120 + dy * 2); + s_lowres[(size_t) y * FX3D_W + x] = mul565_u8(rgb565(6u, 5u, 2u), shade); + } + continue; + } + + const uint32_t t_q15 = (1ul << 30) / (uint32_t) abs_dir_x; + const uint16_t corr = (uint16_t) cos_q15((uint8_t) off); + uint32_t dist_q15 = (uint32_t) (((uint64_t) t_q15 * corr) >> 15); + if (dist_q15 == 0u) dist_q15 = 1u; + int slice = (int) (((uint32_t) FX3D_H << 15) / dist_q15); + slice = clampv(slice, 1, FX3D_H); + int y0 = horizon - slice / 2; + int y1 = y0 + slice - 1; + y0 = clampv(y0, 0, FX3D_H - 1); + y1 = clampv(y1, 0, FX3D_H - 1); + + const int32_t zhit_q15 = (int32_t) (((int64_t) dir_z * (int64_t) t_q15) >> 15); + int u = (int) (((zhit_q15 >> 9) + (int32_t) zscroll) & 63); + if (dir_x < 0) u ^= 63; + const int shade = clampv(255 - (int) (dist_q15 >> 9), 0, 255); + for (int y = y0; y <= y1; y++) { + const int v = ((y - y0) * 64) / ((slice == 0) ? 1 : slice); + uint16_t color = s_ray_tex[(size_t) (v & 63) * kRayTexSize + (size_t) (u & 63)]; + s_lowres[(size_t) y * FX3D_W + x] = mul565_u8(color, (uint8_t) shade); + } + for (int y = y1 + 1; y < FX3D_H; y++) { + const uint16_t k = s_ray_floor_q12[y]; + if (k == 0u) continue; + const int32_t uu_q12 = (int32_t) (((int64_t) dir_x * k) >> 15); + const int32_t vv_q12 = (int32_t) (((int64_t) dir_z * k) >> 15); + const int uf = (int) (((uu_q12 >> 6) + (int32_t) zscroll) & 63); + const int vf = (int) (((vv_q12 >> 6) + (int32_t) (zscroll >> 1)) & 63); + uint16_t color = s_ray_tex[(size_t) (vf & 63) * kRayTexSize + (size_t) (uf & 63)]; + const int dy = y - horizon; + const int fade = clampv(255 - dy * 2, 0, 255); + s_lowres[(size_t) y * FX3D_W + x] = mul565_u8(color, (uint8_t) fade); + } + } +} + +} // namespace + +extern "C" bool fx3d_init(void) { + if (s_ready) return true; + + s_lowres = (uint16_t *) psram_alloc((size_t) FX3D_W * FX3D_H * sizeof(uint16_t)); + s_roto_tex = (uint16_t *) psram_alloc((size_t) kRotoTexSize * kRotoTexSize * sizeof(uint16_t)); + s_ray_tex = (uint16_t *) psram_alloc((size_t) kRayTexSize * kRayTexSize * sizeof(uint16_t)); + s_dots = (DotPt *) psram_alloc((size_t) kDotCount * sizeof(DotPt)); + if (!s_lowres || !s_roto_tex || !s_ray_tex || !s_dots) { + heap_caps_free(s_lowres); s_lowres = nullptr; + heap_caps_free(s_roto_tex); s_roto_tex = nullptr; + heap_caps_free(s_ray_tex); s_ray_tex = nullptr; + heap_caps_free(s_dots); s_dots = nullptr; + return false; + } + + for (int i = 0; i < 256; i++) { + s_sin_q15[i] = (int16_t) (sinf((float) i * (6.28318530f / 256.0f)) * 32767.0f); + } + + // Rotozoom texture — radial-shaded checker (FxEngine::begin). + for (int y = 0; y < kRotoTexSize; y++) { + for (int x = 0; x < kRotoTexSize; x++) { + const float cx = (float) x - kRotoTexSize * 0.5f; + const float cy = (float) y - kRotoTexSize * 0.5f; + float rr = sqrtf(cx * cx + cy * cy) / (kRotoTexSize * 0.5f); + if (rr > 1.0f) rr = 1.0f; + const bool checker = (((x >> 4) ^ (y >> 4)) & 1) != 0; + const uint8_t r = checker ? (uint8_t) (40 + (uint8_t) (200.0f * (1.0f - rr))) : 200u; + const uint8_t g = checker ? (uint8_t) (60 + (uint8_t) (160.0f * (1.0f - rr))) + : (uint8_t) (50 + (uint8_t) (120.0f * (1.0f - rr))); + const uint8_t b = checker ? 200u : (uint8_t) (60 + (uint8_t) (120.0f * (1.0f - rr))); + s_roto_tex[(size_t) y * kRotoTexSize + x] = rgb565(r, g, b); + } + } + + // Dot sphere — shade LUT + random unit sphere (initModeState, same seed). + { + const uint16_t base = rgb565(40u, 80u, 240u); + const uint16_t high = rgb565(255u, 255u, 255u); + for (int i = 0; i < 256; i++) { + const uint8_t diffuse = (uint8_t) i; + const int spec_i = (i > 220) ? (i - 220) * 7 : 0; + const uint8_t spec = (uint8_t) clampv(spec_i, 0, 255); + s_dot_shade[i] = add_sat565(mul565_u8(base, diffuse), mul565_u8(high, spec)); + } + uint32_t rng = 0xBADC0FFEul ^ (uint32_t) FX3D_W ^ ((uint32_t) FX3D_H << 16); + for (int i = 0; i < kDotCount; i++) { + rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; + const uint8_t a = (uint8_t) (rng & 0xFFu); + rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; + const uint8_t b = (uint8_t) ((rng >> 8) & 0xFFu); + const int16_t ca = cos_q15(a), sa = sin_q15(a); + const int16_t cb = cos_q15(b), sb = sin_q15(b); + s_dots[i].x = (int16_t) ((((int32_t) ca * cb) >> 15) >> 8); + s_dots[i].y = (int16_t) ((int32_t) sb >> 8); + s_dots[i].z = (int16_t) ((((int32_t) sa * cb) >> 15) >> 8); + } + } + + // Ray corridor — wall/floor texture, per-column angle offsets, floor LUT + // (initModeState kRayCorridor). + for (int y = 0; y < kRayTexSize; y++) { + for (int x = 0; x < kRayTexSize; x++) { + const bool checker = (((x >> 3) ^ (y >> 3)) & 1) != 0; + int c = checker ? 190 : 70; + if ((y & 7) == 0) c = 40; + s_ray_tex[(size_t) y * kRayTexSize + x] = + rgb565((uint8_t) c, (uint8_t) (c / 2), (uint8_t) (c / 3)); + } + } + for (int x = 0; x < FX3D_W; x++) { + const int dx = x - FX3D_W / 2; + s_ray_col_off[x] = (int8_t) clampv((dx * 24) / (FX3D_W / 2), -64, 64); + } + for (int y = 0; y < FX3D_H; y++) { + const int dy = y - FX3D_H / 2; + if (dy <= 0) { s_ray_floor_q12[y] = 0u; continue; } + uint32_t value = (64ul << 12) / (uint32_t) dy; + if (value > 65535ul) value = 65535ul; + s_ray_floor_q12[y] = (uint16_t) value; + } + + s_ready = true; + return true; +} + +extern "C" void fx3d_render(fx3d_mode_t mode, uint32_t t_ms, uint16_t *dst, + int dst_w, int dst_h) { + if (!s_ready || dst == nullptr || dst_w != FX3D_W * 2 || dst_h != FX3D_H * 2) { + return; + } + switch (mode) { + case FX3D_ROTOZOOM: render_rotozoom(t_ms); break; + case FX3D_DOTSPHERE: render_dotsphere(t_ms); break; + case FX3D_CORRIDOR: render_corridor(t_ms); break; + default: return; + } + // 2x pixel doubling, two 32-bit writes per source pixel, row duplicated. + for (int y = 0; y < FX3D_H; y++) { + const uint16_t *src = &s_lowres[(size_t) y * FX3D_W]; + uint32_t *out0 = (uint32_t *) (dst + (size_t) (y * 2) * dst_w); + uint32_t *out1 = (uint32_t *) (dst + (size_t) (y * 2 + 1) * dst_w); + for (int x = 0; x < FX3D_W; x++) { + const uint32_t px = (uint32_t) src[x] | ((uint32_t) src[x] << 16); + out0[x] = px; + out1[x] = px; + } + } +}