raytrace2.c

C source for Real-time ray tracing on a round ESP32 display, 2061 lines. Raw file

#include "raytrace2.h"

#include <math.h>
#include <string.h>

#include "esp_attr.h"
#include "esp_heap_caps.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define PI 3.14159265358979323846f
#define CX (RT2_W / 2)
#define CY (RT2_H / 2)
#define RT_EPS 0.004f
#define T_MAX 80.0f

typedef struct {
    float x, y, z;
} vec3;

typedef struct {
    vec3 c;
    float r;
    uint8_t cr, cg, cb;
    uint8_t metal;
} sphere_t;

typedef struct {
    int x0, y0, x1, y1;
} rect_t;

typedef struct {
    uint8_t hit;
    uint8_t flags;
    uint8_t cr, cg, cb;
    vec3 p;
    vec3 rorig;
    vec3 rdir;
} corner_t;

typedef struct {
    vec3 eye, fwd, right, up, light;
    float focal;
    float floor_lit;
    sphere_t sph[RT2_NS];
    rect_t rect[RT2_NS];
    rect_t refl[RT2_NS][RT2_NS]; /* cell bbox: j visible in i's reflection */
    rect_t shad[RT2_NS];         /* cell bbox: i's shadow on the floor */
    float ssx[RT2_NS], ssy[RT2_NS], ssr[RT2_NS]; /* screen disk, no pad */
} scene_t;

#define PH_MARK    0
#define PH_PREP    1
#define PH_CORNERS 2
#define PH_FILL    3
#define PH_BG      4

#define RT2_PIVOT_X 0.0f
#define RT2_PIVOT_Y 0.38f
#define RT2_PIVOT_Z 0.15f
#define RT2_EYE0_Y  1.22f
#define RT2_EYE0_Z  (-3.55f)
#define RT2_BG_FOG  0.22f

static TaskHandle_t s_worker;
static TaskHandle_t s_waiter;
static raytrace2_t *s_job;
static scene_t *s_scene;
static int s_phase;
static int s_half_s;
static int s_half_sc;
static int s_half_sh;

static uint16_t s_svis[RT2_GW * RT2_GH];
static uint16_t s_shvis[RT2_GW * RT2_GH];
static uint16_t s_scvis[RT2_CW * RT2_CH];
static uint8_t s_need[RT2_CW * RT2_CH];
static uint8_t s_dirty[RT2_GW * RT2_GH];
static int s_svis_n;
static int s_shvis_n;
static int s_scvis_n;
static rect_t s_last_obj[RT2_NS];
static rect_t s_cur[RT2_NS];
static uint32_t s_fps_shown;
static int s_bg_write_px;
static int s_skip_restore;
static float s_cam_r;
static float s_cam_p0;

static int cell_under_sphere_disk(const scene_t *s, int gx, int gy);

static inline vec3 v3(float x, float y, float z)
{
    vec3 r = { x, y, z };
    return r;
}

static inline vec3 vadd(vec3 a, vec3 b) { return v3(a.x + b.x, a.y + b.y, a.z + b.z); }
static inline vec3 vsub(vec3 a, vec3 b) { return v3(a.x - b.x, a.y - b.y, a.z - b.z); }
static inline vec3 vmul(vec3 a, float s) { return v3(a.x * s, a.y * s, a.z * s); }
static inline float vdot(vec3 a, vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }

static inline vec3 vcross(vec3 a, vec3 b)
{
    return v3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
}

static inline vec3 vnorm(vec3 a)
{
    float l = sqrtf(vdot(a, a));
    return (l > 1e-8f) ? vmul(a, 1.0f / l) : a;
}

static inline vec3 vlerp(vec3 a, vec3 b, float t)
{
    return vadd(vmul(a, 1.0f - t), vmul(b, t));
}

static inline vec3 vbilerp(vec3 a, vec3 b, vec3 c, vec3 d, float u, float v)
{
    return vlerp(vlerp(a, b, u), vlerp(c, d, u), v);
}

static inline corner_t *corn(raytrace2_t *t, int cx, int cy)
{
    return &((corner_t *)t->corners)[cy * RT2_CW + cx];
}

static inline int clamp8(int v)
{
    if (v < 0) {
        return 0;
    }
    if (v > 255) {
        return 255;
    }
    return v;
}

static IRAM_ATTR uint16_t rgb565_pack(int r, int g, int b)
{
    uint16_t c = (uint16_t)(((r & 0xf8) << 8) | ((g & 0xfc) << 3) | (b >> 3));
    return (uint16_t)((c << 8) | (c >> 8));
}

static IRAM_ATTR uint16_t rgb565(int r, int g, int b)
{
    if (r < 0) {
        r = 0;
    } else if (r > 255) {
        r = 255;
    }
    if (g < 0) {
        g = 0;
    } else if (g > 255) {
        g = 255;
    }
    if (b < 0) {
        b = 0;
    } else if (b > 255) {
        b = 255;
    }
    return rgb565_pack(r, g, b);
}

static int clampi(int v, int lo, int hi)
{
    if (v < lo) {
        return lo;
    }
    if (v > hi) {
        return hi;
    }
    return v;
}

static void pix_of_corner(int cx, int cy, int *x, int *y)
{
    *x = clampi(cx * RT2_CELL, 0, RT2_W - 1);
    *y = clampi(cy * RT2_CELL, 0, RT2_H - 1);
}

static int cell_on_screen(int gx, int gy)
{
    int x0 = gx * RT2_CELL;
    int y0 = gy * RT2_CELL;
    int x1 = x0 + RT2_CELL;
    int y1 = y0 + RT2_CELL;
    if (x0 >= RT2_W || y0 >= RT2_H) {
        return 0;
    }
    if (x1 > RT2_W) {
        x1 = RT2_W;
    }
    if (y1 > RT2_H) {
        y1 = RT2_H;
    }
    int qx = (CX < x0) ? x0 : ((CX >= x1) ? x1 - 1 : CX);
    int qy = (CY < y0) ? y0 : ((CY >= y1) ? y1 - 1 : CY);
    int dx = qx - CX;
    int dy = qy - CY;
    return dx * dx + dy * dy <= CX * CX;
}

/* Unnormalized camera ray. Floor hit is t = -eye.y / d.y, p = eye + t*d. */
static IRAM_ATTR vec3 ray_raw(const scene_t *s, float px, float py)
{
    return vadd(vadd(vmul(s->fwd, s->focal), vmul(s->right, px - (float)CX)),
                vmul(s->up, (float)CY - py));
}

/* Normalized. tmin on closest_sphere is in world units. */
static IRAM_ATTR vec3 ray_dir(const scene_t *s, float px, float py)
{
    return vnorm(ray_raw(s, px, py));
}

/* Works for unnormalized rd: a t^2 + 2 b t + c = 0. No normalize. */
static IRAM_ATTR int hit_sphere(vec3 ro, vec3 rd, vec3 c, float r, float tmin, float tmax, float *t)
{
    vec3 oc = vsub(ro, c);
    float a = vdot(rd, rd);
    if (a < 1e-12f) {
        return 0;
    }
    float b = vdot(oc, rd);
    float disc = b * b - a * (vdot(oc, oc) - r * r);
    if (disc < 0.0f) {
        return 0;
    }
    float sd = sqrtf(disc);
    float h = (-b - sd) / a;
    if (h < tmin || h > tmax) {
        h = (-b + sd) / a;
        if (h < tmin || h > tmax) {
            return 0;
        }
    }
    *t = h;
    return 1;
}

static void want_at_px(const scene_t *s, int x, int y, uint8_t want[RT2_NS])
{
    int gx = clampi(x / RT2_CELL, 0, RT2_GW - 1);
    int gy = clampi(y / RT2_CELL, 0, RT2_GH - 1);
    for (int i = 0; i < RT2_NS; i++) {
        const rect_t *rc = &s->rect[i];
        want[i] = (gx >= rc->x0 && gx < rc->x1 && gy >= rc->y0 && gy < rc->y1) ? 1 : 0;
    }
}

static IRAM_ATTR uint8_t closest_sphere(const scene_t *s, vec3 ro, vec3 rd, float tmin,
                              const uint8_t *want, uint8_t skip, float *t_out, vec3 *n_out)
{
    float best = T_MAX;
    uint8_t id = RT2_HIT_NONE;
    vec3 n = v3(0, 1, 0);
    for (int i = 0; i < RT2_NS; i++) {
        uint8_t sid = (uint8_t)(RT2_HIT_SPHERE + i);
        if (sid == skip || (want && !want[i])) {
            continue;
        }
        float t;
        if (!hit_sphere(ro, rd, s->sph[i].c, s->sph[i].r, tmin, best, &t)) {
            continue;
        }
        best = t;
        id = sid;
        n = vnorm(vsub(vadd(ro, vmul(rd, t)), s->sph[i].c));
    }
    *t_out = best;
    *n_out = n;
    return id;
}

static vec3 normal_of(const scene_t *s, uint8_t id, vec3 p)
{
    if (id < RT2_HIT_SPHERE) {
        return v3(0, 1, 0);
    }
    return vnorm(vsub(p, s->sph[id - RT2_HIT_SPHERE].c));
}

static void snap_sphere(const scene_t *s, uint8_t id, vec3 *p)
{
    if (id >= RT2_HIT_SPHERE) {
        int i = id - RT2_HIT_SPHERE;
        *p = vadd(s->sph[i].c, vmul(vnorm(vsub(*p, s->sph[i].c)), s->sph[i].r));
    }
}

static IRAM_ATTR int in_shadow(const scene_t *s, vec3 p, vec3 n, uint8_t skip)
{
    vec3 ro = vadd(p, vmul(n, RT_EPS * 4.0f));
    for (int i = 0; i < RT2_NS; i++) {
        if ((uint8_t)(RT2_HIT_SPHERE + i) == skip) {
            continue;
        }
        float t;
        if (hit_sphere(ro, s->light, s->sph[i].c, s->sph[i].r, RT_EPS, T_MAX, &t)) {
            return 1;
        }
    }
    return 0;
}

static void project_spheres(scene_t *s)
{
    for (int i = 0; i < RT2_NS; i++) {
        vec3 rel = vsub(s->sph[i].c, s->eye);
        float z = vdot(rel, s->fwd);
        rect_t *rc = &s->rect[i];
        if (z < 0.12f) {
            rc->x0 = rc->y0 = rc->x1 = rc->y1 = 0;
            s->ssr[i] = 0.0f;
            continue;
        }
        float x = vdot(rel, s->right);
        float y = vdot(rel, s->up);
        float sx = (float)CX + (x / z) * s->focal;
        float sy = (float)CY - (y / z) * s->focal;
        s->ssx[i] = sx;
        s->ssy[i] = sy;
        s->ssr[i] = (s->sph[i].r / z) * s->focal;
        float sr = s->ssr[i] + (float)RT2_CELL;
        rc->x0 = clampi((int)floorf((sx - sr) / (float)RT2_CELL) - 1, 0, RT2_GW);
        rc->y0 = clampi((int)floorf((sy - sr) / (float)RT2_CELL) - 1, 0, RT2_GH);
        rc->x1 = clampi((int)ceilf((sx + sr) / (float)RT2_CELL) + 1, 0, RT2_GW);
        rc->y1 = clampi((int)ceilf((sy + sr) / (float)RT2_CELL) + 1, 0, RT2_GH);
    }
}

static void rect_clear(rect_t *r)
{
    r->x0 = r->y0 = r->x1 = r->y1 = 0;
}

static int rect_empty(const rect_t *r)
{
    return r->x1 <= r->x0 || r->y1 <= r->y0;
}

static void rect_intersect(rect_t *r, const rect_t *a)
{
    if (r->x0 < a->x0) {
        r->x0 = a->x0;
    }
    if (r->y0 < a->y0) {
        r->y0 = a->y0;
    }
    if (r->x1 > a->x1) {
        r->x1 = a->x1;
    }
    if (r->y1 > a->y1) {
        r->y1 = a->y1;
    }
    if (rect_empty(r)) {
        rect_clear(r);
    }
}

static void rect_union(rect_t *a, const rect_t *b)
{
    if (rect_empty(b)) {
        return;
    }
    if (rect_empty(a)) {
        *a = *b;
        return;
    }
    if (b->x0 < a->x0) {
        a->x0 = b->x0;
    }
    if (b->y0 < a->y0) {
        a->y0 = b->y0;
    }
    if (b->x1 > a->x1) {
        a->x1 = b->x1;
    }
    if (b->y1 > a->y1) {
        a->y1 = b->y1;
    }
}

static void obj_dirty_cells(const scene_t *s, int i, rect_t *d)
{
    *d = s->rect[i];
    rect_union(d, &s->shad[i]);
    if (!rect_empty(d)) {
        d->x0 = clampi(d->x0 - 1, 0, RT2_GW);
        d->y0 = clampi(d->y0 - 1, 0, RT2_GH);
        d->x1 = clampi(d->x1 + 1, 0, RT2_GW);
        d->y1 = clampi(d->y1 + 1, 0, RT2_GH);
    }
}

static void add_flush(raytrace2_t *t, int x0, int y0, int x1, int y1)
{
    if (t->flush_n >= RT2_FLUSH_MAX || x0 >= x1 || y0 >= y1) {
        return;
    }
    if (x0 < 0) {
        x0 = 0;
    }
    if (y0 < 0) {
        y0 = 0;
    }
    if (x1 > RT2_W) {
        x1 = RT2_W;
    }
    if (y1 > RT2_H) {
        y1 = RT2_H;
    }
    rt2_flush_t *f = &t->flush[t->flush_n++];
    f->x0 = (int16_t)x0;
    f->y0 = (int16_t)y0;
    f->x1 = (int16_t)x1;
    f->y1 = (int16_t)y1;
}

static void add_flush_cells(raytrace2_t *t, const rect_t *c)
{
    if (rect_empty(c)) {
        return;
    }
    int x1 = c->x1 * RT2_CELL;
    int y1 = c->y1 * RT2_CELL;
    if (x1 > RT2_W) {
        x1 = RT2_W;
    }
    if (y1 > RT2_H) {
        y1 = RT2_H;
    }
    add_flush(t, c->x0 * RT2_CELL, c->y0 * RT2_CELL, x1, y1);
}

static void restore_cells(raytrace2_t *t, const rect_t *d)
{
    if (rect_empty(d)) {
        return;
    }
    int x0 = d->x0 * RT2_CELL;
    int y0 = d->y0 * RT2_CELL;
    int x1 = d->x1 * RT2_CELL;
    int y1 = d->y1 * RT2_CELL;
    if (x1 > RT2_W) {
        x1 = RT2_W;
    }
    if (y1 > RT2_H) {
        y1 = RT2_H;
    }
    if (x0 >= x1 || y0 >= y1) {
        return;
    }
    size_t nbytes = (size_t)(x1 - x0) * 2u;
    for (int y = y0; y < y1; y++) {
        memcpy(t->pixels + y * RT2_W + x0, t->bg + y * RT2_W + x0, nbytes);
    }
}

static void mark_rect_dirty(const rect_t *r)
{
    if (rect_empty(r)) {
        return;
    }
    for (int y = r->y0; y < r->y1; y++) {
        memset(s_dirty + y * RT2_GW + r->x0, 1, (size_t)(r->x1 - r->x0));
    }
}

static int rects_overlap(const rect_t *a, const rect_t *b)
{
    return a->x0 < b->x1 && b->x0 < a->x1 && a->y0 < b->y1 && b->y0 < a->y1;
}

static int merge_overlap_rects(rect_t *b, int n)
{
    int merged = 1;
    while (merged) {
        merged = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (!rects_overlap(&b[i], &b[j])) {
                    continue;
                }
                rect_union(&b[i], &b[j]);
                b[j] = b[n - 1];
                n--;
                merged = 1;
                break;
            }
            if (merged) {
                break;
            }
        }
    }
    return n;
}

static void restore_dirty_stride(raytrace2_t *t, int gy0, int step)
{
    const scene_t *s = s_scene;
    for (int gy = gy0; gy < RT2_GH; gy += step) {
        const uint8_t *row = s_dirty + gy * RT2_GW;
        int gx = 0;
        while (gx < RT2_GW) {
            while (gx < RT2_GW &&
                   (!row[gx] || (s && cell_under_sphere_disk(s, gx, gy)))) {
                gx++;
            }
            if (gx >= RT2_GW) {
                break;
            }
            int gx0 = gx;
            while (gx < RT2_GW && row[gx] &&
                   !(s && cell_under_sphere_disk(s, gx, gy))) {
                gx++;
            }
            rect_t d = { gx0, gy, gx, gy + 1 };
            restore_cells(t, &d);
        }
    }
}

static void rect_union_disk(rect_t *r, float sx, float sy, float rad, int pad)
{
    int x0 = clampi((int)floorf((sx - rad) / (float)RT2_CELL) - pad, 0, RT2_GW);
    int y0 = clampi((int)floorf((sy - rad) / (float)RT2_CELL) - pad, 0, RT2_GH);
    int x1 = clampi((int)ceilf((sx + rad) / (float)RT2_CELL) + pad, 0, RT2_GW);
    int y1 = clampi((int)ceilf((sy + rad) / (float)RT2_CELL) + pad, 0, RT2_GH);
    if (rect_empty(r)) {
        r->x0 = x0;
        r->y0 = y0;
        r->x1 = x1;
        r->y1 = y1;
        return;
    }
    if (x0 < r->x0) {
        r->x0 = x0;
    }
    if (y0 < r->y0) {
        r->y0 = y0;
    }
    if (x1 > r->x1) {
        r->x1 = x1;
    }
    if (y1 > r->y1) {
        r->y1 = y1;
    }
}

static int project_world(const scene_t *s, vec3 p, float *sx, float *sy, float *z)
{
    vec3 rel = vsub(p, s->eye);
    *z = vdot(rel, s->fwd);
    if (*z < 0.12f) {
        return 0;
    }
    *sx = (float)CX + (vdot(rel, s->right) / *z) * s->focal;
    *sy = (float)CY - (vdot(rel, s->up) / *z) * s->focal;
    return 1;
}

/*
 * Conservative screen box for "sphere j reflected in i".
 * Union of: Blinn bisector blob, inverted-j-in-i, and the limb if the
 * hit sits on the back hemisphere. Always clipped to i's disk, padded.
 */
static void project_refl_boxes(scene_t *s)
{
    for (int i = 0; i < RT2_NS; i++) {
        const rect_t *host = &s->rect[i];
        for (int j = 0; j < RT2_NS; j++) {
            rect_t *rb = &s->refl[i][j];
            rect_clear(rb);
            if (i == j || rect_empty(host)) {
                continue;
            }
            vec3 C = s->sph[i].c;
            float ar = s->sph[i].r;
            vec3 Q = s->sph[j].c;
            float br = s->sph[j].r;
            vec3 toB = vsub(Q, C);
            float d2 = vdot(toB, toB);
            vec3 toE = vsub(s->eye, C);
            float e2 = vdot(toE, toE);
            if (d2 < 1e-6f || e2 < 1e-6f) {
                *rb = *host;
                continue;
            }
            float d = sqrtf(d2);
            float el = sqrtf(e2);
            vec3 u = vmul(toB, 1.0f / d);
            vec3 v = vmul(toE, 1.0f / el);
            float zA, sxA, syA;
            if (!project_world(s, C, &sxA, &syA, &zA)) {
                *rb = *host;
                continue;
            }
            float srA = (ar / zA) * s->focal;
            float ang = br / (d - ar > 0.08f ? (d - ar) : 0.08f);
            if (ang > 1.8f) {
                ang = 1.8f;
            }
            float blob = srA * ang * 3.2f + (float)RT2_CELL * 2.0f;

            vec3 bis = vadd(v, u);
            float bl2 = vdot(bis, bis);
            if (bl2 > 0.04f) {
                vec3 n = vmul(bis, 1.0f / sqrtf(bl2));
                vec3 P = vadd(C, vmul(n, ar));
                float sx, sy, z;
                if (project_world(s, P, &sx, &sy, &z)) {
                    float grow = blob;
                    float facing = vdot(n, v);
                    if (facing < 0.35f) {
                        grow += srA * 0.55f;
                    }
                    rect_union_disk(rb, sx, sy, grow, 2);
                }
            }

            /* Sphere inversion of j through i — extra coverage, viewer-independent. */
            float L = d2 - br * br;
            if (L > 0.08f || L < -0.08f) {
                float k = (ar * ar) / L;
                vec3 Qp = vadd(C, vmul(toB, k));
                float sp = fabsf((ar * ar) * br / L);
                float sx, sy, z;
                if (sp < ar * 2.5f && project_world(s, Qp, &sx, &sy, &z)) {
                    float zq = z > 0.12f ? z : 0.12f;
                    float spr = (sp / zq) * s->focal + blob * 0.35f;
                    rect_union_disk(rb, sx, sy, spr, 2);
                }
            } else {
                *rb = *host;
                continue;
            }

            /* Back-hemisphere / opposite: image piles on the limb toward B. */
            if (bl2 <= 0.04f || vdot(u, v) < -0.15f) {
                float dx = 0.0f, dy = 0.0f, zz;
                float sxB, syB;
                if (project_world(s, Q, &sxB, &syB, &zz)) {
                    dx = sxB - sxA;
                    dy = syB - syA;
                } else {
                    dx = vdot(toB, s->right);
                    dy = -vdot(toB, s->up);
                }
                float len = sqrtf(dx * dx + dy * dy);
                if (len > 1.0f) {
                    float lx = sxA + srA * (dx / len);
                    float ly = syA + srA * (dy / len);
                    rect_union_disk(rb, lx, ly, srA * 0.75f + blob, 2);
                } else {
                    *rb = *host;
                    continue;
                }
            }

            if (rect_empty(rb)) {
                *rb = *host;
            } else {
                rect_t clip = *host;
                clip.x0 = clampi(clip.x0 - 1, 0, RT2_GW);
                clip.y0 = clampi(clip.y0 - 1, 0, RT2_GH);
                clip.x1 = clampi(clip.x1 + 1, 0, RT2_GW);
                clip.y1 = clampi(clip.y1 + 1, 0, RT2_GH);
                rect_intersect(rb, &clip);
                if (rect_empty(rb)) {
                    *rb = *host;
                }
            }
        }
    }
}

/* Directional light: sphere ∩ plane y=0 is an ellipse centred on the
 * anti-light projection of C, contained in a circle of radius r / L.y. */
static void union_floor_disk(const scene_t *s, rect_t *r, float px, float pz, float rad)
{
    float sx, sy, z;
    if (project_world(s, v3(px, 0.0f, pz), &sx, &sy, &z)) {
        float sr = (rad / z) * s->focal + (float)RT2_CELL * 1.5f;
        rect_union_disk(r, sx, sy, sr, 1);
    }
    const float ox[4] = { rad, -rad, 0.0f, 0.0f };
    const float oz[4] = { 0.0f, 0.0f, rad, -rad };
    for (int k = 0; k < 4; k++) {
        if (project_world(s, v3(px + ox[k], 0.0f, pz + oz[k]), &sx, &sy, &z)) {
            rect_union_disk(r, sx, sy, (float)RT2_CELL * 1.5f, 1);
        }
    }
}

static void project_shadow_boxes(scene_t *s)
{
    vec3 L = s->light;
    for (int i = 0; i < RT2_NS; i++) {
        rect_t *rb = &s->shad[i];
        rect_clear(rb);
        if (L.y < 0.08f) {
            continue;
        }
        vec3 C = s->sph[i].c;
        float r = s->sph[i].r;
        if (C.y + r < 0.0f) {
            continue;
        }
        float t = C.y / L.y;
        vec3 P0 = vsub(C, vmul(L, t));
        float grad = r / L.y * 1.25f;
        union_floor_disk(s, rb, P0.x, P0.z, grad);
        union_floor_disk(s, rb, C.x, C.z, r + 0.04f);
    }
}

static int cell_in_rect(const rect_t *r, int gx, int gy)
{
    return gx >= r->x0 && gx < r->x1 && gy >= r->y0 && gy < r->y1;
}

static int cell_in_shadow_rect(const scene_t *s, int gx, int gy)
{
    for (int i = 0; i < RT2_NS; i++) {
        if (cell_in_rect(&s->shad[i], gx, gy)) {
            return 1;
        }
    }
    return 0;
}

static int refl_box_overlaps(const rect_t *r, int x0, int y0, int x1, int y1)
{
    if (rect_empty(r)) {
        return 0;
    }
    int rx0 = r->x0 * RT2_CELL;
    int ry0 = r->y0 * RT2_CELL;
    int rx1 = r->x1 * RT2_CELL;
    int ry1 = r->y1 * RT2_CELL;
    return x0 < rx1 && x1 > rx0 && y0 < ry1 && y1 > ry0;
}

static void cam_const_init(void)
{
    if (s_cam_r > 0.0f) {
        return;
    }
    float dy = RT2_EYE0_Y - RT2_PIVOT_Y;
    float dz = RT2_EYE0_Z - RT2_PIVOT_Z;
    s_cam_r = sqrtf(dy * dy + dz * dz);
    s_cam_p0 = atan2f(dy, -dz);
}

static float cam_radius(void)
{
    cam_const_init();
    return s_cam_r;
}

static float cam_pitch0(void)
{
    cam_const_init();
    return s_cam_p0;
}

static void apply_camera(scene_t *s, float yaw, float pitch)
{
    vec3 pivot = v3(RT2_PIVOT_X, RT2_PIVOT_Y, RT2_PIVOT_Z);
    float r = cam_radius();
    float cp = cosf(pitch);
    float sp = sinf(pitch);
    float cy = cosf(yaw);
    float sy = sinf(yaw);
    s->eye = v3(pivot.x + r * cp * sy, pivot.y + r * sp, pivot.z - r * cp * cy);
    s->fwd = vnorm(vsub(pivot, s->eye));
    vec3 world_up = v3(0, 1, 0);
    s->right = vnorm(vcross(s->fwd, world_up));
    if (vdot(s->right, s->right) < 1e-8f) {
        s->right = v3(1, 0, 0);
    }
    s->up = vcross(s->right, s->fwd);
    s->focal = (float)CX / tanf(52.0f * PI / 180.0f * 0.5f);
}

static void setup_scene(scene_t *s, uint32_t frames, float yaw, float pitch)
{
    float a = frames * 0.018f;
    apply_camera(s, yaw, pitch);
    s->light = vnorm(v3(0.45f, 0.88f, -0.30f));
    float ndl = s->light.y;
    if (ndl < 0.0f) {
        ndl = 0.0f;
    }
    s->floor_lit = 0.28f + 0.72f * ndl;

    s->sph[0].c = v3(sinf(a) * 0.95f, 0.58f, cosf(a) * 0.55f);
    s->sph[0].r = 0.56f;
    s->sph[0].cr = 255;
    s->sph[0].cg = 36;
    s->sph[0].cb = 28;
    s->sph[0].metal = 160;

    s->sph[1].c = v3(-1.15f, 0.42f, 0.15f + sinf(a * 0.7f) * 0.2f);
    s->sph[1].r = 0.42f;
    s->sph[1].cr = 16;
    s->sph[1].cg = 196;
    s->sph[1].cb = 230;
    s->sph[1].metal = 150;

    s->sph[2].c = v3(1.05f, 0.32f + fabsf(sinf(a * 1.6f)) * 0.45f, -0.15f);
    s->sph[2].r = 0.32f;
    s->sph[2].cr = 255;
    s->sph[2].cg = 200;
    s->sph[2].cb = 24;
    s->sph[2].metal = 80;

    s->sph[3].c = v3(0.15f, 0.28f, 1.15f);
    s->sph[3].r = 0.28f;
    s->sph[3].cr = 236;
    s->sph[3].cg = 236;
    s->sph[3].cb = 242;
    s->sph[3].metal = 230;
}

static IRAM_ATTR void mix_fog(int *r, int *g, int *b, float f)
{
    if (f <= 0.0f) {
        return;
    }
    if (f > 1.0f) {
        f = 1.0f;
    }
    const int fr = 118, fg = 126, fb = 148;
    *r += (int)((fr - *r) * f);
    *g += (int)((fg - *g) * f);
    *b += (int)((fb - *b) * f);
}

static inline int ifloor1p6(float x)
{
    float f = x * 1.6f;
    int i = (int)f;
    if (f < 0.0f && f != (float)i) {
        i--;
    }
    return i;
}

static inline int ifloorf(float x)
{
    int i = (int)x;
    if (x < 0.0f && x != (float)i) {
        i--;
    }
    return i;
}

static void checker_rgb(vec3 p, int *r, int *g, int *b)
{
    int ix = ifloor1p6(p.x);
    int iz = ifloor1p6(p.z);
    if (((ix + iz) & 1) == 0) {
        *r = 248;
        *g = 236;
        *b = 214;
    } else {
        *r = 28;
        *g = 26;
        *b = 32;
    }
}

static IRAM_ATTR void shade_sky(vec3 d, float fog_k, int *r, int *g, int *b)
{
    float len2 = vdot(d, d);
    float ny = 0.0f;
    if (d.y > 0.0f && len2 > 1e-8f) {
        ny = (d.y * d.y) / len2;
    }
    *r = (int)(18 + ny * 70);
    *g = (int)(28 + ny * 90);
    *b = (int)(70 + ny * 120);
    float fog = 1.0f - ny;
    mix_fog(r, g, b, fog * fog * fog_k);
}

/* Analytic floor or sky from an unnormalized ray. fog_k 0..1.
 * Floor n=(0,1,0): rd·n < 0 and origin above y=0, else sky. */
static IRAM_ATTR void shade_env(vec3 ro, vec3 rd, float floor_lit, float fog_k, int *r, int *g, int *b)
{
    if (rd.y >= 0.0f || ro.y <= 0.0f) {
        shade_sky(rd, fog_k, r, g, b);
        return;
    }
    float t = -ro.y / rd.y;
    vec3 p = vadd(ro, vmul(rd, t));
    checker_rgb(p, r, g, b);
    *r = (int)(*r * floor_lit);
    *g = (int)(*g * floor_lit);
    *b = (int)(*b * floor_lit);
    float dist2 = p.x * p.x + p.z * p.z;
    float fog = (dist2 - 16.0f) * (1.0f / 90.0f);
    float len2 = vdot(rd, rd);
    float down = (len2 > 1e-8f) ? ((rd.y * rd.y) / len2) : 0.0f;
    float horiz = (1.0f - down);
    horiz = horiz * horiz;
    if (horiz > fog) {
        fog = horiz;
    }
    mix_fog(r, g, b, fog * fog_k);
}

static void shade_sphere_lit(const scene_t *s, uint8_t id, vec3 n, int *r, int *g, int *b)
{
    int i = id - RT2_HIT_SPHERE;
    float ndl = vdot(n, s->light);
    if (ndl < 0.0f) {
        ndl = 0.0f;
    }
    float lit = 0.28f + 0.72f * ndl;
    *r = clamp8((int)(s->sph[i].cr * lit));
    *g = clamp8((int)(s->sph[i].cg * lit));
    *b = clamp8((int)(s->sph[i].cb * lit));
}

static int floor_point(const scene_t *s, float px, float py, vec3 *p)
{
    vec3 d = ray_raw(s, px, py);
    if (d.y >= -1e-6f) {
        return 0;
    }
    float t = -s->eye.y / d.y;
    if (t <= 0.0f) {
        return 0;
    }
    *p = vadd(s->eye, vmul(d, t));
    p->y = 0.0f;
    return 1;
}

static int cell_in_sphere_rect(const scene_t *s, int gx, int gy, int pad)
{
    for (int k = 0; k < RT2_NS; k++) {
        const rect_t *rc = &s->rect[k];
        if (gx >= rc->x0 - pad && gx < rc->x1 + pad &&
            gy >= rc->y0 - pad && gy < rc->y1 + pad) {
            return 1;
        }
    }
    return 0;
}

static void mark_sphere_cells(const scene_t *s, const raytrace2_t *t)
{
    memset(s_need, 0, sizeof(s_need));
    s_svis_n = 0;
    for (int i = 0; i < t->vis_n; i++) {
        int gx = t->vis[i] & 0xff;
        int gy = t->vis[i] >> 8;
        if (!cell_in_sphere_rect(s, gx, gy, 0)) {
            continue;
        }
        s_svis[s_svis_n++] = t->vis[i];
        s_need[gy * RT2_CW + gx] = 1;
        s_need[gy * RT2_CW + gx + 1] = 1;
        s_need[(gy + 1) * RT2_CW + gx] = 1;
        s_need[(gy + 1) * RT2_CW + gx + 1] = 1;
    }
    s_scvis_n = 0;
    for (int i = 0; i < RT2_CW * RT2_CH; i++) {
        if (s_need[i]) {
            s_scvis[s_scvis_n++] = (uint16_t)((i % RT2_CW) | ((i / RT2_CW) << 8));
        }
    }
    s_half_s = s_svis_n / 2;
    s_half_sc = s_scvis_n / 2;
}

/* True only if the whole 8×8 sits inside a projected sphere disk. */
static int cell_under_sphere_disk(const scene_t *s, int gx, int gy)
{
    int x0 = gx * RT2_CELL;
    int y0 = gy * RT2_CELL;
    int x1 = x0 + RT2_CELL - 1;
    int y1 = y0 + RT2_CELL - 1;
    for (int k = 0; k < RT2_NS; k++) {
        float sr = s->ssr[k] - 1.5f;
        if (sr < (float)RT2_CELL) {
            continue;
        }
        float sx = s->ssx[k];
        float sy = s->ssy[k];
        float fx = (fabsf((float)x0 - sx) > fabsf((float)x1 - sx)) ? (float)x0 : (float)x1;
        float fy = (fabsf((float)y0 - sy) > fabsf((float)y1 - sy)) ? (float)y0 : (float)y1;
        float dx = fx - sx;
        float dy = fy - sy;
        if (dx * dx + dy * dy < sr * sr) {
            return 1;
        }
    }
    return 0;
}

static void mark_shadow_cells(const scene_t *s, const raytrace2_t *t)
{
    s_shvis_n = 0;
    for (int i = 0; i < t->vis_n; i++) {
        int gx = t->vis[i] & 0xff;
        int gy = t->vis[i] >> 8;
        if (cell_in_shadow_rect(s, gx, gy) && !cell_under_sphere_disk(s, gx, gy)) {
            s_shvis[s_shvis_n++] = t->vis[i];
        }
    }
    s_half_sh = s_shvis_n / 2;
}

static void mark_object_dirty(const scene_t *s, int i)
{
    obj_dirty_cells(s, i, &s_cur[i]);
    mark_rect_dirty(&s_last_obj[i]);
    mark_rect_dirty(&s_cur[i]);
}

static void cache_primary(const scene_t *s, corner_t *c)
{
    if (c->hit < RT2_HIT_SPHERE) {
        c->cr = c->cg = c->cb = 0;
        return;
    }
    int i = c->hit - RT2_HIT_SPHERE;
    vec3 n = normal_of(s, c->hit, c->p);
    float ndl = vdot(n, s->light);
    if (ndl < 0.0f) {
        ndl = 0.0f;
    }
    float lit = 0.28f + ((c->flags & RT2_SHADOW_P) ? 0.0f : 0.72f * ndl);
    c->cr = (uint8_t)clamp8((int)(s->sph[i].cr * lit));
    c->cg = (uint8_t)clamp8((int)(s->sph[i].cg * lit));
    c->cb = (uint8_t)clamp8((int)(s->sph[i].cb * lit));
}

static void set_refl_unnorm(const scene_t *s, corner_t *c, vec3 view)
{
    vec3 n = normal_of(s, c->hit, c->p);
    /* I - 2(I·N)N, left unnormalized so interpolation has no sqrt. */
    c->rdir = vsub(view, vmul(n, 2.0f * vdot(view, n)));
    c->rorig = vadd(c->p, vmul(n, RT_EPS * 4.0f));
}

static void trace_full(const scene_t *s, int x, int y, corner_t *c)
{
    c->hit = 0;
    c->flags = 0;
    vec3 rd = ray_dir(s, (float)x, (float)y);
    uint8_t want[RT2_NS];
    want_at_px(s, x, y, want);
    float t;
    vec3 n;
    c->hit = closest_sphere(s, s->eye, rd, 0.02f, want, RT2_HIT_NONE, &t, &n);
    if (c->hit == RT2_HIT_NONE) {
        return;
    }
    c->p = vadd(s->eye, vmul(rd, t));
    set_refl_unnorm(s, c, rd);
    if (in_shadow(s, c->p, n, c->hit)) {
        c->flags |= RT2_SHADOW_P;
    }
    cache_primary(s, c);
}

static void sample_mid(const scene_t *s, int x, int y,
                       const corner_t *c00, const corner_t *c10,
                       const corner_t *c01, const corner_t *c11,
                       float u, float v, int same_hit, int same_sp,
                       corner_t *out)
{
    out->hit = 0;
    out->flags = 0;
    if (same_hit) {
        out->hit = c00->hit;
        if (out->hit >= RT2_HIT_SPHERE) {
            out->p = vbilerp(c00->p, c10->p, c01->p, c11->p, u, v);
            snap_sphere(s, out->hit, &out->p);
            out->rorig = vbilerp(c00->rorig, c10->rorig, c01->rorig, c11->rorig, u, v);
            out->rdir = vbilerp(c00->rdir, c10->rdir, c01->rdir, c11->rdir, u, v);
        }
    } else {
        vec3 view = ray_dir(s, (float)x, (float)y);
        uint8_t want[RT2_NS];
        want_at_px(s, x, y, want);
        float t;
        vec3 n;
        out->hit = closest_sphere(s, s->eye, view, 0.02f, want, RT2_HIT_NONE, &t, &n);
        if (out->hit >= RT2_HIT_SPHERE) {
            out->p = vadd(s->eye, vmul(view, t));
            set_refl_unnorm(s, out, view);
        }
    }
    if (out->hit < RT2_HIT_SPHERE) {
        return;
    }
    if (same_sp) {
        out->flags |= (uint8_t)(c00->flags & RT2_SHADOW_P);
    } else if (in_shadow(s, out->p, normal_of(s, out->hit, out->p), out->hit)) {
        out->flags |= RT2_SHADOW_P;
    }
    cache_primary(s, out);
}

static int all_eq4(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
    return a == b && a == c && a == d;
}

static IRAM_ATTR void fill_sphere(raytrace2_t *t, const scene_t *s,
                                  int x0, int y0, int x1, int y1,
                                  const corner_t *c00, const corner_t *c10,
                                  const corner_t *c01, const corner_t *c11)
{
    int w = x1 - x0;
    int h = y1 - y0;
    if (w <= 0 || h <= 0) {
        return;
    }
    uint8_t metal = 0;
    uint8_t want[RT2_NS];
    int any_refl = 0;
    int si = 0;
    if (c00->hit >= RT2_HIT_SPHERE) {
        si = c00->hit - RT2_HIT_SPHERE;
        metal = s->sph[si].metal;
        for (int j = 0; j < RT2_NS; j++) {
            want[j] = 0;
            if (j == si) {
                continue;
            }
            if (refl_box_overlaps(&s->refl[si][j], x0, y0, x1, y1)) {
                want[j] = 1;
                any_refl = 1;
            }
        }
    }
    float invw = 1.0f / (float)w;
    float invh = 1.0f / (float)h;
    int cr_l = (int)c00->cr << 8, cr_r = (int)c10->cr << 8;
    int cg_l = (int)c00->cg << 8, cg_r = (int)c10->cg << 8;
    int cb_l = (int)c00->cb << 8, cb_r = (int)c10->cb << 8;
    int dcr_l = (((int)c01->cr - (int)c00->cr) << 8) / h;
    int dcr_r = (((int)c11->cr - (int)c10->cr) << 8) / h;
    int dcg_l = (((int)c01->cg - (int)c00->cg) << 8) / h;
    int dcg_r = (((int)c11->cg - (int)c10->cg) << 8) / h;
    int dcb_l = (((int)c01->cb - (int)c00->cb) << 8) / h;
    int dcb_r = (((int)c11->cb - (int)c10->cb) << 8) / h;
    int nm = 256 - (int)metal;
    float lit = s->floor_lit;
    int nsx = w / 2 + 1;
    int nsy = h / 2 + 1;
    int env_r[36], env_g[36], env_b[36];
    if (metal && nsx * nsy <= 36) {
        for (int iy = 0; iy < nsy; iy++) {
            int py = y0 + iy * 2;
            if (py >= y1) {
                py = y1 - 1;
            }
            float vf = ((float)(py - y0) + 0.5f) * invh;
            for (int ix = 0; ix < nsx; ix++) {
                int px = x0 + ix * 2;
                if (px >= x1) {
                    px = x1 - 1;
                }
                float uf = ((float)(px - x0) + 0.5f) * invw;
                vec3 ro = vbilerp(c00->rorig, c10->rorig, c01->rorig, c11->rorig, uf, vf);
                vec3 rd = vbilerp(c00->rdir, c10->rdir, c01->rdir, c11->rdir, uf, vf);
                int er, eg, eb;
                uint8_t hid = RT2_HIT_NONE;
                if (any_refl) {
                    uint8_t swant[RT2_NS];
                    int gx = px / RT2_CELL;
                    int gy = py / RT2_CELL;
                    int any = 0;
                    if (gx < 0) {
                        gx = 0;
                    } else if (gx >= RT2_GW) {
                        gx = RT2_GW - 1;
                    }
                    if (gy < 0) {
                        gy = 0;
                    } else if (gy >= RT2_GH) {
                        gy = RT2_GH - 1;
                    }
                    for (int j = 0; j < RT2_NS; j++) {
                        swant[j] = (want[j] && cell_in_rect(&s->refl[si][j], gx, gy)) ? 1 : 0;
                        any |= swant[j];
                    }
                    if (any) {
                        float ht;
                        vec3 n;
                        hid = closest_sphere(s, ro, rd, RT_EPS, swant, c00->hit, &ht, &n);
                        if (hid >= RT2_HIT_SPHERE) {
                            shade_sphere_lit(s, hid, n, &er, &eg, &eb);
                        }
                    }
                }
                if (hid < RT2_HIT_SPHERE) {
                    shade_env(ro, rd, lit, 0.70f, &er, &eg, &eb);
                }
                int i = iy * nsx + ix;
                env_r[i] = er;
                env_g[i] = eg;
                env_b[i] = eb;
            }
        }
    }
    for (int y = y0; y < y1; y++) {
        uint16_t *row = t->pixels + y * RT2_W;
        int cr = cr_l, cg = cg_l, cb = cb_l;
        int dcr = (cr_r - cr_l) / w;
        int dcg = (cg_r - cg_l) / w;
        int dcb = (cb_r - cb_l) / w;
        int oy = y - y0;
        int iy = oy >> 1;
        int iy1 = iy + 1;
        if (iy1 >= nsy) {
            iy1 = nsy - 1;
        }
        unsigned fy = (unsigned)(oy & 1) << 7;
        for (int x = x0; x < x1; x++) {
            int r = cr >> 8;
            int g = cg >> 8;
            int b = cb >> 8;
            if (metal) {
                int ox = x - x0;
                int ix = ox >> 1;
                int ix1 = ix + 1;
                if (ix1 >= nsx) {
                    ix1 = nsx - 1;
                }
                unsigned fx = (unsigned)(ox & 1) << 7;
                int i00 = iy * nsx + ix;
                int i10 = iy * nsx + ix1;
                int i01 = iy1 * nsx + ix;
                int i11 = iy1 * nsx + ix1;
                int top_r = env_r[i00] + (((env_r[i10] - env_r[i00]) * (int)fx) >> 8);
                int bot_r = env_r[i01] + (((env_r[i11] - env_r[i01]) * (int)fx) >> 8);
                int top_g = env_g[i00] + (((env_g[i10] - env_g[i00]) * (int)fx) >> 8);
                int bot_g = env_g[i01] + (((env_g[i11] - env_g[i01]) * (int)fx) >> 8);
                int top_b = env_b[i00] + (((env_b[i10] - env_b[i00]) * (int)fx) >> 8);
                int bot_b = env_b[i01] + (((env_b[i11] - env_b[i01]) * (int)fx) >> 8);
                int er = top_r + (((bot_r - top_r) * (int)fy) >> 8);
                int eg = top_g + (((bot_g - top_g) * (int)fy) >> 8);
                int eb = top_b + (((bot_b - top_b) * (int)fy) >> 8);
                r = (r * nm + er * (int)metal) >> 8;
                g = (g * nm + eg * (int)metal) >> 8;
                b = (b * nm + eb * (int)metal) >> 8;
            }
            row[x] = rgb565_pack(r, g, b);
            cr += dcr;
            cg += dcg;
            cb += dcb;
        }
        cr_l += dcr_l;
        cr_r += dcr_r;
        cg_l += dcg_l;
        cg_r += dcg_r;
        cb_l += dcb_l;
        cb_r += dcb_r;
    }
}

static void fill_quad(raytrace2_t *t, const scene_t *s,
                      int x0, int y0, int x1, int y1,
                      const corner_t *c00, const corner_t *c10,
                      const corner_t *c01, const corner_t *c11)
{
    if (x0 >= x1 || y0 >= y1) {
        return;
    }
    int same_hit = all_eq4(c00->hit, c10->hit, c01->hit, c11->hit);
    int w = x1 - x0;
    int h = y1 - y0;

    if (same_hit && c00->hit == RT2_HIT_NONE) {
        return;
    }
    if (same_hit && c00->hit >= RT2_HIT_SPHERE) {
        fill_sphere(t, s, x0, y0, x1, y1, c00, c10, c01, c11);
        return;
    }
    if (w == 1 && h == 1) {
        if (c00->hit >= RT2_HIT_SPHERE) {
            fill_sphere(t, s, x0, y0, x1, y1, c00, c10, c01, c11);
        }
        return;
    }

    int mx = x0 + w / 2;
    int my = y0 + h / 2;
    if (mx == x0) {
        mx = x1;
    }
    if (my == y0) {
        my = y1;
    }
    float uw = (w > 0) ? (float)(mx - x0) / (float)w : 0.0f;
    float vh = (h > 0) ? (float)(my - y0) / (float)h : 0.0f;
    corner_t cM0, cM1, c0M, c1M, cMM;
    if (mx < x1) {
        sample_mid(s, mx, y0, c00, c10, c01, c11, uw, 0.0f, same_hit, 0, &cM0);
        sample_mid(s, mx, y1, c00, c10, c01, c11, uw, 1.0f, same_hit, 0, &cM1);
    } else {
        cM0 = *c10;
        cM1 = *c11;
    }
    if (my < y1) {
        sample_mid(s, x0, my, c00, c10, c01, c11, 0.0f, vh, same_hit, 0, &c0M);
        sample_mid(s, x1, my, c00, c10, c01, c11, 1.0f, vh, same_hit, 0, &c1M);
    } else {
        c0M = *c01;
        c1M = *c11;
    }
    if (mx < x1 && my < y1) {
        sample_mid(s, mx, my, c00, c10, c01, c11, uw, vh, same_hit, 0, &cMM);
    } else {
        cMM = cM0;
    }
    fill_quad(t, s, x0, y0, mx, my, c00, &cM0, &c0M, &cMM);
    if (mx < x1) {
        fill_quad(t, s, mx, y0, x1, my, &cM0, c10, &cMM, &c1M);
    }
    if (my < y1) {
        fill_quad(t, s, x0, my, mx, y1, &c0M, &cMM, c01, &cM1);
    }
    if (mx < x1 && my < y1) {
        fill_quad(t, s, mx, my, x1, y1, &cMM, &c1M, &cM1, c11);
    }
}

static uint16_t darken_565(uint16_t c, unsigned k)
{
    c = (uint16_t)((c << 8) | (c >> 8));
    unsigned r5 = ((c >> 11) & 31u) * k >> 8;
    unsigned g6 = ((c >> 5) & 63u) * k >> 8;
    unsigned b5 = (c & 31u) * k >> 8;
    c = (uint16_t)((r5 << 11) | (g6 << 5) | b5);
    return (uint16_t)((c << 8) | (c >> 8));
}

static void darken_floor(raytrace2_t *t, const scene_t *s,
                         int x0, int y0, int x1, int y1, int test_floor)
{
    unsigned k = (unsigned)(0.28f / s->floor_lit * 256.0f);
    if (k > 256u) {
        k = 256u;
    }
    for (int y = y0; y < y1; y++) {
        uint16_t *row = t->pixels + y * RT2_W;
        const uint16_t *src = t->bg + y * RT2_W;
        for (int x = x0; x < x1; x++) {
            if (test_floor) {
                vec3 p;
                if (!floor_point(s, (float)x + 0.5f, (float)y + 0.5f, &p)) {
                    continue;
                }
            }
            row[x] = darken_565(src[x], k);
        }
    }
}

/* Directional umbra on y=0 is the ellipse (cylinder ∩ plane). Same as a
 * unit-L ray-sphere test, no sqrt. c<=0: floor point inside the ball. */
static IRAM_ATTR int ellipse_shadows_floor(vec3 C, float r, vec3 L, vec3 p)
{
    float ocx = p.x - C.x;
    float ocy = -C.y;
    float ocz = p.z - C.z;
    float b = ocx * L.x + ocy * L.y + ocz * L.z;
    float c = ocx * ocx + ocy * ocy + ocz * ocz - r * r;
    if (c <= 0.0f) {
        return 1;
    }
    return b < 0.0f && (b * b >= c);
}

static int in_floor_shadow(const scene_t *s, vec3 p, int x, int y)
{
    int gx = x / RT2_CELL;
    int gy = y / RT2_CELL;
    if (gx < 0) {
        gx = 0;
    } else if (gx >= RT2_GW) {
        gx = RT2_GW - 1;
    }
    if (gy < 0) {
        gy = 0;
    } else if (gy >= RT2_GH) {
        gy = RT2_GH - 1;
    }
    vec3 L = s->light;
    for (int i = 0; i < RT2_NS; i++) {
        if (!cell_in_rect(&s->shad[i], gx, gy)) {
            continue;
        }
        if (ellipse_shadows_floor(s->sph[i].c, s->sph[i].r, L, p)) {
            return 1;
        }
    }
    return 0;
}

static int corner_floor_shadow(const scene_t *s, int x, int y)
{
    vec3 p;
    if (!floor_point(s, (float)x, (float)y, &p)) {
        return 0;
    }
    return in_floor_shadow(s, p, x, y);
}

static void fill_bg_quad(raytrace2_t *t, const scene_t *s,
                         int x0, int y0, int x1, int y1,
                         int sh00, int sh10, int sh01, int sh11)
{
    int w = x1 - x0;
    int h = y1 - y0;
    if (sh00 == sh10 && sh00 == sh01 && sh00 == sh11) {
        if (sh00) {
            darken_floor(t, s, x0, y0, x1, y1, 0);
        }
        return;
    }
    if (w == 1 && h == 1) {
        if (sh00) {
            darken_floor(t, s, x0, y0, x1, y1, 1);
        }
        return;
    }
    int mx = x0 + w / 2;
    int my = y0 + h / 2;
    if (mx == x0) {
        mx = x1;
    }
    if (my == y0) {
        my = y1;
    }
    int shM0 = (mx < x1) ? corner_floor_shadow(s, mx, y0) : sh10;
    int shM1 = (mx < x1) ? corner_floor_shadow(s, mx, y1) : sh11;
    int sh0M = (my < y1) ? corner_floor_shadow(s, x0, my) : sh01;
    int sh1M = (my < y1) ? corner_floor_shadow(s, x1, my) : sh11;
    int shMM = (mx < x1 && my < y1) ? corner_floor_shadow(s, mx, my) : shM0;
    fill_bg_quad(t, s, x0, y0, mx, my, sh00, shM0, sh0M, shMM);
    if (mx < x1) {
        fill_bg_quad(t, s, mx, y0, x1, my, shM0, sh10, shMM, sh1M);
    }
    if (my < y1) {
        fill_bg_quad(t, s, x0, my, mx, y1, sh0M, shMM, sh01, shM1);
    }
    if (mx < x1 && my < y1) {
        fill_bg_quad(t, s, mx, my, x1, y1, shMM, sh1M, shM1, sh11);
    }
}

static int floor_uv_at(const scene_t *s, float px, float py, float *u, float *v)
{
    vec3 d = ray_raw(s, px, py);
    if (d.y >= 0.0f || s->eye.y <= 0.0f) {
        return 0;
    }
    float t = -s->eye.y / d.y;
    *u = (s->eye.x + d.x * t) * 1.6f;
    *v = (s->eye.z + d.z * t) * 1.6f;
    return 1;
}

static uint16_t sky_color(const scene_t *s, float px, float py)
{
    vec3 d = ray_raw(s, px, py);
    int r, g, b;
    shade_sky(d, RT2_BG_FOG, &r, &g, &b);
    return rgb565(r, g, b);
}

static void checker_pair(const scene_t *s, float fog, uint16_t *c0, uint16_t *c1)
{
    int r0 = (int)(248 * s->floor_lit);
    int g0 = (int)(236 * s->floor_lit);
    int b0 = (int)(214 * s->floor_lit);
    int r1 = (int)(28 * s->floor_lit);
    int g1 = (int)(26 * s->floor_lit);
    int b1 = (int)(32 * s->floor_lit);
    mix_fog(&r0, &g0, &b0, fog);
    mix_fog(&r1, &g1, &b1, fog);
    *c0 = rgb565(r0, g0, b0);
    *c1 = rgb565(r1, g1, b1);
}

static float scanline_fog(const scene_t *s, float px, float py)
{
    vec3 d = ray_raw(s, px, py);
    if (d.y >= -1e-6f || s->eye.y <= 0.0f) {
        return 0.0f;
    }
    float t = -s->eye.y / d.y;
    float x = s->eye.x + d.x * t;
    float z = s->eye.z + d.z * t;
    float dist2 = x * x + z * z;
    float fog = (dist2 - 16.0f) * (1.0f / 90.0f);
    float len2 = vdot(d, d);
    float down = (len2 > 1e-8f) ? ((d.y * d.y) / len2) : 0.0f;
    float horiz = (1.0f - down);
    horiz = horiz * horiz;
    if (horiz > fog) {
        fog = horiz;
    }
    return fog * RT2_BG_FOG;
}

/* Plane UV at the visible left/right of the scanline, lerp across. One fog. */
static void fill_bg_row(raytrace2_t *t, const scene_t *s, int y, int write_px)
{
    int dy = y - CY;
    int rem = CX * CX - dy * dy;
    if (rem <= 0) {
        return;
    }
    int rx = (int)sqrtf((float)rem);
    int xa = CX - rx;
    int xb = CX + rx + 1;
    if (xa < 0) {
        xa = 0;
    }
    if (xb > RT2_W) {
        xb = RT2_W;
    }
    if (xa >= xb) {
        return;
    }
    float py = (float)y + 0.5f;
    float xl = (float)xa + 0.5f;
    float xr = (float)(xb - 1) + 0.5f;
    vec3 dL = ray_raw(s, xl, py);
    vec3 dR = ray_raw(s, xr, py);
    int flL = (dL.y < 0.0f && s->eye.y > 0.0f);
    int flR = (dR.y < 0.0f && s->eye.y > 0.0f);
    uint16_t *bg = t->bg + y * RT2_W;
    uint16_t *px = t->pixels + y * RT2_W;
    int x_floor0 = xa;
    int x_floor1 = xb;

    if (!flL && !flR) {
        uint16_t c = sky_color(s, (xl + xr) * 0.5f, py);
        for (int x = xa; x < xb; x++) {
            bg[x] = c;
            if (write_px) {
                px[x] = c;
            }
        }
        return;
    }
    if (flL != flR) {
        float ddy = dR.y - dL.y;
        float u = (fabsf(ddy) > 1e-8f) ? (-dL.y / ddy) : 0.5f;
        if (u < 0.0f) {
            u = 0.0f;
        }
        if (u > 1.0f) {
            u = 1.0f;
        }
        int xh = xa + (int)(u * (float)(xb - xa) + 0.5f);
        if (xh < xa) {
            xh = xa;
        }
        if (xh > xb) {
            xh = xb;
        }
        if (flL) {
            x_floor0 = xa;
            x_floor1 = xh;
        } else {
            x_floor0 = xh;
            x_floor1 = xb;
        }
        int xs0 = flL ? xh : xa;
        int xs1 = flL ? xb : xh;
        if (xs0 < xs1) {
            uint16_t c = sky_color(s, (float)(xs0 + xs1) * 0.5f, py);
            for (int x = xs0; x < xs1; x++) {
                bg[x] = c;
                if (write_px) {
                    px[x] = c;
                }
            }
        }
    }
    if (x_floor1 <= x_floor0) {
        return;
    }
    float u0, v0, u1, v1;
    float xf0 = (float)x_floor0 + 0.5f;
    float xf1 = (float)(x_floor1 - 1) + 0.5f;
    if (!floor_uv_at(s, xf0, py, &u0, &v0) || !floor_uv_at(s, xf1, py, &u1, &v1)) {
        uint16_t c = sky_color(s, (xf0 + xf1) * 0.5f, py);
        for (int x = x_floor0; x < x_floor1; x++) {
            bg[x] = c;
            if (write_px) {
                px[x] = c;
            }
        }
        return;
    }
    int n = x_floor1 - x_floor0;
    float du = (u1 - u0) / (float)n;
    float dv = (v1 - v0) / (float)n;
    float fog = scanline_fog(s, (xf0 + xf1) * 0.5f, py);
    uint16_t c0, c1;
    checker_pair(s, fog, &c0, &c1);
    float u = u0;
    float v = v0;
    for (int x = x_floor0; x < x_floor1; x++) {
        uint16_t c = (((ifloorf(u) + ifloorf(v)) & 1) == 0) ? c0 : c1;
        bg[x] = c;
        if (write_px) {
            px[x] = c;
        }
        u += du;
        v += dv;
    }
}

static void fill_bg_scanlines(raytrace2_t *t, const scene_t *s, int y0, int step, int write_px)
{
    for (int y = y0; y < RT2_H; y += step) {
        fill_bg_row(t, s, y, write_px);
    }
}

static void prerender_background(raytrace2_t *t)
{
    static scene_t scene;
    setup_scene(&scene, 0, t->yaw, t->pitch);
    fill_bg_scanlines(t, &scene, 0, 1, 0);
}

static void fill_background(raytrace2_t *t, const scene_t *s, int start, int end)
{
    for (int i = start; i < end; i++) {
        int gx = s_shvis[i] & 0xff;
        int gy = s_shvis[i] >> 8;
        int x0 = gx * RT2_CELL;
        int y0 = gy * RT2_CELL;
        int x1 = x0 + RT2_CELL;
        int y1 = y0 + RT2_CELL;
        if (x1 > RT2_W) {
            x1 = RT2_W;
        }
        if (y1 > RT2_H) {
            y1 = RT2_H;
        }
        int sh00 = corner_floor_shadow(s, x0, y0);
        int sh10 = corner_floor_shadow(s, x1, y0);
        int sh01 = corner_floor_shadow(s, x0, y1);
        int sh11 = corner_floor_shadow(s, x1, y1);
        fill_bg_quad(t, s, x0, y0, x1, y1, sh00, sh10, sh01, sh11);
    }
}

static void trace_corners(raytrace2_t *t, const scene_t *s, int start, int end)
{
    for (int i = start; i < end; i++) {
        uint16_t v = s_scvis[i];
        int cx = v & 0xff;
        int cy = v >> 8;
        int x, y;
        pix_of_corner(cx, cy, &x, &y);
        trace_full(s, x, y, corn(t, cx, cy));
    }
}

static void fill_spheres(raytrace2_t *t, const scene_t *s, int start, int end)
{
    for (int i = start; i < end; i++) {
        uint16_t v = s_svis[i];
        int gx = v & 0xff;
        int gy = v >> 8;
        int x0 = gx * RT2_CELL;
        int y0 = gy * RT2_CELL;
        int x1 = x0 + RT2_CELL;
        int y1 = y0 + RT2_CELL;
        if (x1 > RT2_W) {
            x1 = RT2_W;
        }
        if (y1 > RT2_H) {
            y1 = RT2_H;
        }
        fill_quad(t, s, x0, y0, x1, y1,
                  corn(t, gx, gy), corn(t, gx + 1, gy),
                  corn(t, gx, gy + 1), corn(t, gx + 1, gy + 1));
    }
}

static void worker_task(void *arg)
{
    (void)arg;
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        if (s_phase == PH_MARK) {
            mark_shadow_cells(s_scene, s_job);
            mark_object_dirty(s_scene, 2);
            mark_object_dirty(s_scene, 3);
        } else if (s_phase == PH_PREP) {
            if (!s_skip_restore) {
                restore_dirty_stride(s_job, 1, 2);
            }
            fill_background(s_job, s_scene, s_half_sh, s_shvis_n);
        } else if (s_phase == PH_CORNERS) {
            trace_corners(s_job, s_scene, s_half_sc, s_scvis_n);
        } else if (s_phase == PH_BG) {
            fill_bg_scanlines(s_job, s_scene, 1, 2, s_bg_write_px);
        } else {
            fill_spheres(s_job, s_scene, s_half_s, s_svis_n);
        }
        xTaskNotifyGive(s_waiter);
    }
}

int raytrace2_init(raytrace2_t *t)
{
    memset(t, 0, sizeof(*t));
    t->pixels = heap_caps_aligned_alloc(64, (size_t)RT2_W * RT2_H * 2,
                                        MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
    t->bg = heap_caps_malloc((size_t)RT2_W * RT2_H * 2,
                             MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    t->corners = heap_caps_malloc((size_t)RT2_CW * RT2_CH * sizeof(corner_t),
                                  MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    t->vis = heap_caps_malloc((size_t)RT2_GW * RT2_GH * sizeof(uint16_t),
                              MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
    t->cvis = heap_caps_malloc((size_t)RT2_CW * RT2_CH * sizeof(uint16_t),
                               MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
    if (!t->vis) {
        t->vis = heap_caps_malloc((size_t)RT2_GW * RT2_GH * sizeof(uint16_t),
                                  MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    }
    if (!t->cvis) {
        t->cvis = heap_caps_malloc((size_t)RT2_CW * RT2_CH * sizeof(uint16_t),
                                   MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
    }
    if (!t->pixels || !t->bg || !t->corners || !t->vis || !t->cvis) {
        raytrace2_free(t);
        return -1;
    }
    memset(t->pixels, 0, (size_t)RT2_W * RT2_H * 2);
    memset(t->bg, 0, (size_t)RT2_W * RT2_H * 2);

    t->vis_n = 0;
    for (int gy = 0; gy < RT2_GH; gy++) {
        for (int gx = 0; gx < RT2_GW; gx++) {
            if (cell_on_screen(gx, gy)) {
                t->vis[t->vis_n++] = (uint16_t)(gx | (gy << 8));
            }
        }
    }
    t->cvis_n = 0;

    if (s_worker == NULL) {
        if (xTaskCreatePinnedToCore(worker_task, "rt2", 8192, NULL, 5, &s_worker, 1) != pdPASS) {
            raytrace2_free(t);
            return -1;
        }
    }
    t->yaw = 0.0f;
    t->pitch = cam_pitch0();
    t->view_dirty = 0;
    t->show_boxes = 0;
    t->force_flush = 0;
    prerender_background(t);
    memcpy(t->pixels, t->bg, (size_t)RT2_W * RT2_H * 2);
    for (int i = 0; i < RT2_NS; i++) {
        rect_clear(&s_last_obj[i]);
    }
    t->flush_n = 0;
    add_flush(t, 0, 0, RT2_W, RT2_H);
    s_fps_shown = 0;
    t->fps_t0 = esp_timer_get_time();
    return 0;
}

void raytrace2_orbit(raytrace2_t *t, float dx, float dy)
{
    if (dx * dx + dy * dy < 4.0f) {
        return;
    }
    float r = cam_radius();
    float ny = t->yaw + dx * 0.006f;
    float np = t->pitch + dy * 0.005f;
    float smin = (0.12f - RT2_PIVOT_Y) / r;
    if (smin < -0.99f) {
        smin = -0.99f;
    }
    if (smin > 0.99f) {
        smin = 0.99f;
    }
    float pmin = asinf(smin);
    const float pmax = 1.35f;
    if (np < pmin) {
        np = pmin;
    }
    if (np > pmax) {
        np = pmax;
    }
    if (ny == t->yaw && np == t->pitch) {
        return;
    }
    t->yaw = ny;
    t->pitch = np;
    t->view_dirty = 1;
}

void raytrace2_toggle_boxes(raytrace2_t *t)
{
    t->show_boxes ^= 1;
    t->force_flush = 1;
}

void raytrace2_free(raytrace2_t *t)
{
    heap_caps_free(t->pixels);
    heap_caps_free(t->bg);
    heap_caps_free(t->corners);
    heap_caps_free(t->vis);
    heap_caps_free(t->cvis);
    memset(t, 0, sizeof(*t));
}

static void draw_hline(uint16_t *fb, int y, int x0, int x1, uint16_t c)
{
    if (y < 0 || y >= RT2_H) {
        return;
    }
    if (x0 < 0) {
        x0 = 0;
    }
    if (x1 > RT2_W) {
        x1 = RT2_W;
    }
    uint16_t *row = fb + y * RT2_W;
    for (int x = x0; x < x1; x++) {
        row[x] = c;
    }
}

static void draw_vline(uint16_t *fb, int x, int y0, int y1, uint16_t c)
{
    if (x < 0 || x >= RT2_W) {
        return;
    }
    if (y0 < 0) {
        y0 = 0;
    }
    if (y1 > RT2_H) {
        y1 = RT2_H;
    }
    for (int y = y0; y < y1; y++) {
        fb[y * RT2_W + x] = c;
    }
}

static void outline_cell_rect(uint16_t *fb, const rect_t *r, uint16_t c)
{
    if (rect_empty(r)) {
        return;
    }
    int x0 = r->x0 * RT2_CELL;
    int y0 = r->y0 * RT2_CELL;
    int x1 = r->x1 * RT2_CELL;
    int y1 = r->y1 * RT2_CELL;
    if (x1 > RT2_W) {
        x1 = RT2_W;
    }
    if (y1 > RT2_H) {
        y1 = RT2_H;
    }
    if (x0 >= x1 || y0 >= y1) {
        return;
    }
    draw_hline(fb, y0, x0, x1, c);
    draw_hline(fb, y1 - 1, x0, x1, c);
    draw_vline(fb, x0, y0, y1, c);
    draw_vline(fb, x1 - 1, y0, y1, c);
}

static void draw_debug_boxes(raytrace2_t *t, const scene_t *s)
{
    uint16_t *fb = t->pixels;
    uint16_t cref = rgb565(255, 70, 230);
    uint16_t csh = rgb565(255, 220, 40);
    uint16_t csph = rgb565(50, 255, 90);
    for (int i = 0; i < RT2_NS; i++) {
        for (int j = 0; j < RT2_NS; j++) {
            outline_cell_rect(fb, &s->refl[i][j], cref);
        }
        outline_cell_rect(fb, &s->shad[i], csh);
        outline_cell_rect(fb, &s->rect[i], csph);
    }
}

void raytrace2_frame(raytrace2_t *t)
{
    static scene_t scene;
    setup_scene(&scene, t->frames, t->yaw, t->pitch);

    memset(s_dirty, 0, sizeof(s_dirty));
    s_job = t;
    s_scene = &scene;
    s_waiter = xTaskGetCurrentTaskHandle();

    int view_changed = t->view_dirty;
    if (view_changed) {
        s_bg_write_px = 1;
        s_phase = PH_BG;
        xTaskNotifyGive(s_worker);
        fill_bg_scanlines(t, &scene, 0, 2, 1);
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        t->view_dirty = 0;
        for (int i = 0; i < RT2_NS; i++) {
            rect_clear(&s_last_obj[i]);
        }
    }

    project_spheres(&scene);
    project_refl_boxes(&scene);
    project_shadow_boxes(&scene);

    s_phase = PH_MARK;
    xTaskNotifyGive(s_worker);
    mark_sphere_cells(&scene, t);
    mark_object_dirty(&scene, 0);
    mark_object_dirty(&scene, 1);
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

    t->flush_n = 0;
    if (t->frames < 2 || view_changed || t->force_flush || t->show_boxes) {
        add_flush(t, 0, 0, RT2_W, RT2_H);
        t->force_flush = 0;
    } else {
        rect_t boxes[RT2_NS];
        int nb = 0;
        for (int i = 0; i < RT2_NS; i++) {
            rect_t uni = s_last_obj[i];
            rect_union(&uni, &s_cur[i]);
            if (!rect_empty(&uni)) {
                boxes[nb++] = uni;
            }
        }
        nb = merge_overlap_rects(boxes, nb);
        for (int i = 0; i < nb; i++) {
            add_flush_cells(t, &boxes[i]);
        }
    }
    for (int i = 0; i < RT2_NS; i++) {
        s_last_obj[i] = s_cur[i];
    }

    s_skip_restore = view_changed;
    s_phase = PH_PREP;
    xTaskNotifyGive(s_worker);
    if (!s_skip_restore) {
        restore_dirty_stride(t, 0, 2);
    }
    fill_background(t, &scene, 0, s_half_sh);
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

    s_phase = PH_CORNERS;
    xTaskNotifyGive(s_worker);
    trace_corners(t, &scene, 0, s_half_sc);
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

    s_phase = PH_FILL;
    xTaskNotifyGive(s_worker);
    fill_spheres(t, &scene, 0, s_half_s);
    ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

    if (t->show_boxes) {
        draw_debug_boxes(t, &scene);
    }

    t->frames++;
    t->fps_acc++;
    int64_t now = esp_timer_get_time();
    if (now - t->fps_t0 >= 1000000) {
        t->fps = t->fps_acc;
        t->fps_acc = 0;
        t->fps_t0 = now;
    }
    if (t->frames >= 2) {
        int hit_fps = (t->fps != s_fps_shown);
        for (int i = 0; i < t->flush_n && !hit_fps; i++) {
            if (t->flush[i].y1 > RT2_H - 40) {
                hit_fps = 1;
            }
        }
        if (hit_fps) {
            add_flush(t, 0, RT2_H - 40, RT2_W, RT2_H);
            s_fps_shown = t->fps;
        }
    }
}

static const uint16_t k_digits[10] = {
    0x7b6f, 0x2c97, 0x73e7, 0x73cf, 0x5bc9,
    0x79cf, 0x79ef, 0x7249, 0x7bef, 0x7bcf,
};

static void blit_digit(uint16_t *fb, int ox, int oy, int digit, uint16_t on)
{
    uint16_t bits = k_digits[digit];
    for (int y = 0; y < 5; y++) {
        for (int x = 0; x < 3; x++) {
            if (bits & (1 << (14 - (y * 3 + x)))) {
                int px = ox + x * 4;
                int py = oy + y * 4;
                for (int dy = 0; dy < 3; dy++) {
                    for (int dx = 0; dx < 3; dx++) {
                        fb[(py + dy) * RT2_W + (px + dx)] = on;
                    }
                }
            }
        }
    }
}

void raytrace2_draw_fps(raytrace2_t *t)
{
    char buf[16];
    int n = 0;
    unsigned v = (unsigned)t->fps;
    if (v == 0) {
        buf[n++] = '0';
    } else {
        char tmp[8];
        int m = 0;
        while (v && m < 8) {
            tmp[m++] = (char)('0' + (v % 10));
            v /= 10;
        }
        while (m--) {
            buf[n++] = tmp[m];
        }
    }
    const int scale_w = 3 * 4 + 4;
    int x = (RT2_W - n * scale_w) / 2;
    int y = RT2_H - 36;
    uint16_t on = rgb565(255, 255, 255);
    for (int i = 0; i < n; i++) {
        blit_digit(t->pixels, x + i * scale_w, y, buf[i] - '0', on);
    }
}