// 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 <cmath>
#include <cstring>

#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;
constexpr int kStar3DCount = 400;   // ~W*H/50, capped for the 10 ms tick
constexpr int kVoxelMaxDist = 96;   // FxEngine voxel_max_dist_

struct DotPt  { int16_t x, y, z; };
struct Star3D { int16_t x, y; uint16_t 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;
Star3D   *s_stars3d;
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];
uint8_t   s_voxel_height[256];
uint16_t  s_voxel_pal[256];
uint16_t  s_voxel_proj_q8[kVoxelMaxDist + 1];
uint32_t  s_rng = 0x5EED1234u;
bool      s_ready = false;

uint32_t next_rand(void) {
    s_rng ^= s_rng << 13; s_rng ^= s_rng >> 17; s_rng ^= s_rng << 5;
    return s_rng;
}

// ---- 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 <typename T>
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 set_pixel(int x, int y, uint16_t color) {
    if (x < 0 || y < 0 || x >= FX3D_W || y >= FX3D_H) return;
    s_lowres[(size_t) y * FX3D_W + (size_t) x] = color;
}

// Bresenham, additive (v9 WireCubeFx::line_, max-blend approximated by add).
void add_line(int x0, int y0, int x1, int y1, uint16_t color) {
    int dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
    const int sx = (x0 < x1) ? 1 : -1;
    int dy = (y1 > y0) ? (y0 - y1) : (y1 - y0);   // -abs
    const int sy = (y0 < y1) ? 1 : -1;
    int err = dx + dy;
    for (;;) {
        add_pixel(x0, y0, color);
        if (x0 == x1 && y0 == y1) break;
        const int e2 = err << 1;
        if (e2 >= dy) { err += dy; x0 += sx; }
        if (e2 <= dx) { err += dx; y0 += sy; }
    }
}

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<int32_t>((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<int>(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<int>(slice, 1, FX3D_H);
        int y0 = horizon - slice / 2;
        int y1 = y0 + slice - 1;
        y0 = clampv<int>(y0, 0, FX3D_H - 1);
        y1 = clampv<int>(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<int>(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<int>(255 - dy * 2, 0, 255);
            s_lowres[(size_t) y * FX3D_W + x] = mul565_u8(color, (uint8_t) fade);
        }
    }
}

// FxEngine::renderStarfield3D — z-flight starfield, slow roll, motion trails.
void render_starfield3d(uint32_t now_ms) {
    fill_lowres(rgb565(2u, 4u, 10u));
    const uint8_t angle = (uint8_t) (now_ms >> 4);
    const int16_t cs = cos_q15(angle);
    const int16_t sn = sin_q15(angle);
    const int fov = ((FX3D_W < FX3D_H) ? FX3D_W : FX3D_H) + 24;
    const uint16_t z_min = 32u;
    const int dz = (int) (10u + ((now_ms >> 6) & 7u));
    const int cx = FX3D_W / 2, cy = FX3D_H / 2;
    const uint16_t base = rgb565(240u, 248u, 255u);

    for (int i = 0; i < kStar3DCount; i++) {
        Star3D &star = s_stars3d[i];
        const uint16_t z_prev = star.z;
        const int z_next = (int) star.z - dz;
        if (z_next < (int) z_min) {
            star.x = (int16_t) ((int32_t) (next_rand() & 511u) - 256);
            star.y = (int16_t) ((int32_t) ((next_rand() >> 9) & 511u) - 256);
            star.z = (uint16_t) (256u + (next_rand() % 768u));
            continue;
        }
        star.z = (uint16_t) z_next;

        const int xr = (int) (((int32_t) star.x * cs - (int32_t) star.y * sn) >> 15);
        const int yr = (int) (((int32_t) star.x * sn + (int32_t) star.y * cs) >> 15);
        const int sx = cx + (xr * fov) / (int) star.z;
        const int sy = cy + (yr * fov) / (int) star.z;
        const int sx0 = cx + (xr * fov) / (int) z_prev;
        const int sy0 = cy + (yr * fov) / (int) z_prev;
        if (sx < 0 || sy < 0 || sx >= FX3D_W || sy >= FX3D_H) continue;

        uint8_t brightness = (uint8_t) (255u - (star.z >> 2));
        if (brightness < 40u) brightness = 40u;
        set_pixel(sx, sy, mul565_u8(base, brightness));

        const int dx = sx - sx0, dy = sy - sy0;
        int steps = ((dx < 0 ? -dx : dx) > (dy < 0 ? -dy : dy))
                        ? (dx < 0 ? -dx : dx) : (dy < 0 ? -dy : dy);
        steps = clampv<int>(steps, 0, 10);
        for (int s = 1; s <= steps; s++) {
            const int x = sx0 + (dx * s) / steps;
            const int y = sy0 + (dy * s) / steps;
            const uint8_t fade = (uint8_t) ((brightness * (steps - s)) / (steps + 1));
            add_pixel(x, y, mul565_u8(base, fade));
        }
    }
}

// FxEngine::renderVoxelLandscape — per-column raycast heightfield over a
// vertical sky gradient, camera drifting with time.
void render_voxel(uint32_t now_ms) {
    for (int y = 0; y < FX3D_H; y++) {
        const uint8_t t = (uint8_t) ((y * 255) / FX3D_H);
        const uint16_t color = rgb565((uint8_t) ((8 * (255 - t)) >> 8),
                                      (uint8_t) ((12 * (255 - t)) >> 8),
                                      (uint8_t) ((32 * (255 - t)) >> 8));
        const size_t row = (size_t) y * FX3D_W;
        for (int x = 0; x < FX3D_W; x++) s_lowres[row + x] = color;
    }

    const int horizon = FX3D_H / 2;
    const uint8_t angle = (uint8_t) (now_ms >> 6);
    const uint16_t cam_x = (uint16_t) ((now_ms >> 5) & 255u);
    const uint16_t cam_y = (uint16_t) ((now_ms >> 6) & 255u);
    const int half = FX3D_W / 2;

    for (int x = 0; x < FX3D_W; x++) {
        const int dx = x - half;
        const uint8_t ray_angle = (uint8_t) (angle + (uint8_t) ((dx * 24) / half));
        const int16_t dir_x = cos_q15(ray_angle);
        const int16_t dir_y = sin_q15(ray_angle);
        int max_y = FX3D_H - 1;
        for (int z = 1; z <= kVoxelMaxDist; z++) {
            const int map_x = ((int) cam_x + ((dir_x * z) >> 15)) & 255;
            const int map_y = ((int) cam_y + ((dir_y * z) >> 15)) & 255;
            const uint8_t hh = s_voxel_height[(uint8_t) ((map_x + map_y * 3) & 255)];
            const uint16_t proj = s_voxel_proj_q8[z];
            int y = horizon - (int) (((unsigned) hh * proj) >> 8);
            if (y < 0) y = 0;
            if (y > max_y) continue;
            const uint8_t shade = (uint8_t) ((z * 3 < 255) ? (255 - z * 3) : 0);
            const uint16_t color = s_voxel_pal[shade];
            for (int yy = y; yy <= max_y; yy++)
                s_lowres[(size_t) yy * FX3D_W + x] = color;
            max_y = y - 1;
            if (max_y < 0) break;
        }
    }
}

// v9 WireCubeFx — wireframe cube, float math (8 vertices/frame, FPU is fine),
// rotation speeds and projection from the v9 defaults feel.
void render_wirecube(uint32_t now_ms) {
    fill_lowres(rgb565(2u, 3u, 8u));
    static const float V[8][3] = {
        {-1, -1, -1}, {+1, -1, -1}, {+1, +1, -1}, {-1, +1, -1},
        {-1, -1, +1}, {+1, -1, +1}, {+1, +1, +1}, {-1, +1, +1},
    };
    static const uint8_t E[12][2] = {
        {0, 1}, {1, 2}, {2, 3}, {3, 0},
        {4, 5}, {5, 6}, {6, 7}, {7, 4},
        {0, 4}, {1, 5}, {2, 6}, {3, 7},
    };
    const float t = (float) now_ms * 0.001f;
    const float ax = t * 0.9f, ay = t * 1.3f, az = t * 0.5f;
    const float sx = sinf(ax), cxr = cosf(ax);
    const float sy = sinf(ay), cyr = cosf(ay);
    const float sz = sinf(az), czr = cosf(az);
    const int cx = FX3D_W / 2, cy = FX3D_H / 2;
    const float pulse = 0.5f + 0.5f * sinf(t * 2.4f);
    const float scale = (float) FX3D_H * 0.30f * (1.0f + 0.20f * pulse);
    const float fov = 2.2f, z_offset = 3.0f;

    int px[8], py[8];
    for (int i = 0; i < 8; i++) {
        const float x = V[i][0], y = V[i][1], z = V[i][2];
        const float x1 = x * cyr + z * sy;
        const float z1 = -x * sy + z * cyr;
        const float y2 = y * cxr - z1 * sx;
        const float z2 = y * sx + z1 * cxr;
        const float x3 = x1 * czr - y2 * sz;
        const float y3 = x1 * sz + y2 * czr;
        float zz = z2 + z_offset;
        if (zz < 0.3f) zz = 0.3f;
        const float inv = fov / zz;
        px[i] = cx + (int) lroundf(x3 * inv * scale);
        py[i] = cy + (int) lroundf(y3 * inv * scale);
    }
    const uint16_t edge = mul565_u8(rgb565(120u, 255u, 220u),
                                    (uint8_t) (200 + (int) (55.0f * pulse)));
    for (int e = 0; e < 12; e++)
        add_line(px[E[e][0]], py[E[e][0]], px[E[e][1]], py[E[e][1]], edge);
}

}  // 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));
    s_stars3d = (Star3D *) psram_alloc((size_t) kStar3DCount * sizeof(Star3D));
    if (!s_lowres || !s_roto_tex || !s_ray_tex || !s_dots || !s_stars3d) {
        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;
        heap_caps_free(s_stars3d);  s_stars3d = 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<int>(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<int>((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;
    }

    // Starfield 3D — initModeState kStarfield3D.
    for (int i = 0; i < kStar3DCount; i++) {
        s_stars3d[i].x = (int16_t) ((int32_t) (next_rand() & 511u) - 256);
        s_stars3d[i].y = (int16_t) ((int32_t) ((next_rand() >> 9) & 511u) - 256);
        s_stars3d[i].z = (uint16_t) (128u + (next_rand() % 896u));
    }

    // Voxel landscape — initModeState kVoxelLandscape (sine heightfield,
    // shaded green palette, perspective projection table).
    for (int i = 0; i < 256; i++) {
        const int16_t s1 = sin_q15((uint8_t) i);
        const int16_t s2 = sin_q15((uint8_t) (i * 3));
        s_voxel_height[i] = (uint8_t) clampv<int>(s1 / 512 + s2 / 1024 + 128, 0, 255);
        s_voxel_pal[i] = mul565_u8(rgb565(30u, 220u, 80u), (uint8_t) i);
    }
    for (int z = 1; z <= kVoxelMaxDist; z++) {
        s_voxel_proj_q8[z] = (uint16_t) clampv<int>((70 * 256) / (z + 8), 0, 65535);
    }
    s_voxel_proj_q8[0] = 0u;

    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;
        case FX3D_STARFIELD: render_starfield3d(t_ms); break;
        case FX3D_VOXEL:     render_voxel(t_ms);       break;
        case FX3D_WIRECUBE:  render_wirecube(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;
        }
    }
}
