#include "pxl8_cpu.h"

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

struct pxl8_cpu_render_target {
    u8* framebuffer;
    u32 height;
    u32 width;
    u16* zbuffer;
    u32* light_accum;
};

static inline u16 depth_to_u16(f32 z) {
    f32 d = (z + 1.0f) * 32767.5f;
    if (d < 0.0f) d = 0.0f;
    if (d > 65535.0f) d = 65535.0f;
    return (u16)d;
}

static inline f32 calc_light_intensity(const pxl8_light* light, pxl8_vec3 world_pos, pxl8_vec3 normal) {
    pxl8_vec3 to_light = pxl8_vec3_sub(light->position, world_pos);
    f32 dist_sq = pxl8_vec3_dot(to_light, to_light);

    if (dist_sq >= light->radius_sq) {
        return 0.0f;
    }

    f32 intensity_norm = light->intensity * (1.0f / 255.0f);

    if (dist_sq < 0.001f) {
        return intensity_norm;
    }

    f32 inv_dist = pxl8_fast_inv_sqrt(dist_sq);
    pxl8_vec3 light_dir = pxl8_vec3_scale(to_light, inv_dist);
    f32 n_dot_l = pxl8_vec3_dot(normal, light_dir);

    if (n_dot_l <= 0.0f) {
        return 0.0f;
    }

    f32 falloff = 1.0f - dist_sq * light->inv_radius_sq;
    return n_dot_l * falloff * intensity_norm;
}

typedef struct pxl8_light_result {
    u8 light;
    u32 light_color;
} pxl8_light_result;

static pxl8_light_result calc_vertex_light(
    pxl8_vec3 world_pos,
    pxl8_vec3 normal,
    const pxl8_3d_frame* frame
) {
    f32 intensity = 0.25f + frame->uniforms.ambient * (1.0f / 255.0f);

    f32 accum_r = 0.0f;
    f32 accum_g = 0.0f;
    f32 accum_b = 0.0f;
    f32 total_dynamic = 0.0f;

    f32 celestial_dot = -pxl8_vec3_dot(normal, frame->uniforms.celestial_dir);
    if (celestial_dot > 0.0f) {
        intensity += celestial_dot * frame->uniforms.celestial_intensity;
    }

    f32 sky_factor = normal.y * 0.5f + 0.5f;
    if (sky_factor < 0.0f) sky_factor = 0.0f;
    intensity += sky_factor * frame->uniforms.celestial_intensity * 0.3f;

    for (u32 i = 0; i < frame->uniforms.num_lights; i++) {
        const pxl8_light* light = &frame->uniforms.lights[i];
        f32 contrib = calc_light_intensity(light, world_pos, normal);
        if (contrib > 0.0f) {
            intensity += contrib;
            total_dynamic += contrib;

            accum_r += light->r * contrib;
            accum_g += light->g * contrib;
            accum_b += light->b * contrib;
        }
    }

    u32 light_color = 0;
    if (total_dynamic > 0.001f) {
        f32 inv_total = 1.0f / total_dynamic;
        f32 tint_strength = total_dynamic * 1.5f;
        if (tint_strength > 1.0f) tint_strength = 1.0f;

        u32 r = (u32)(accum_r * inv_total);
        u32 g = (u32)(accum_g * inv_total);
        u32 b = (u32)(accum_b * inv_total);
        u32 a = (u32)(tint_strength * 255.0f);

        if (r > 255) r = 255;
        if (g > 255) g = 255;
        if (b > 255) b = 255;

        light_color = r | (g << 8) | (b << 16) | (a << 24);
    }

    if (intensity < 0.0f) intensity = 0.0f;
    if (intensity > 1.0f) intensity = 1.0f;

    return (pxl8_light_result){
        .light = (u8)(intensity * 255.0f),
        .light_color = light_color,
    };
}

#define PXL8_MAX_TARGET_STACK 8

struct pxl8_cpu_backend {
    pxl8_cpu_render_target* current_target;
    pxl8_cpu_render_target* target_stack[PXL8_MAX_TARGET_STACK];
    u32 target_stack_depth;
    const pxl8_colormap* colormap;
    const pxl8_palette_cube* palette_cube;
    const u32* palette;
    pxl8_3d_frame frame;
    pxl8_mat4 mvp;
    u32* output;
    u32 output_size;
};

static inline void clip_line_2d(i32* x0, i32* y0, i32* x1, i32* y1, i32 w, i32 h, bool* visible) {
    const u8 INSIDE = 0, LEFT = 1, RIGHT = 2, BOTTOM = 4, TOP = 8;

    u8 outcode0 = INSIDE, outcode1 = INSIDE;
    if (*x0 < 0) outcode0 |= LEFT;
    else if (*x0 >= w) outcode0 |= RIGHT;
    if (*y0 < 0) outcode0 |= TOP;
    else if (*y0 >= h) outcode0 |= BOTTOM;

    if (*x1 < 0) outcode1 |= LEFT;
    else if (*x1 >= w) outcode1 |= RIGHT;
    if (*y1 < 0) outcode1 |= TOP;
    else if (*y1 >= h) outcode1 |= BOTTOM;

    *visible = true;
    while (true) {
        if ((outcode0 | outcode1) == 0) return;
        if ((outcode0 & outcode1) != 0) { *visible = false; return; }

        u8 out = outcode0 ? outcode0 : outcode1;
        i32 x, y;
        i64 dx = *x1 - *x0, dy = *y1 - *y0;

        if (out & TOP) { x = *x0 + (i32)(dx * (0 - *y0) / dy); y = 0; }
        else if (out & BOTTOM) { x = *x0 + (i32)(dx * (h - 1 - *y0) / dy); y = h - 1; }
        else if (out & RIGHT) { y = *y0 + (i32)(dy * (w - 1 - *x0) / dx); x = w - 1; }
        else { y = *y0 + (i32)(dy * (0 - *x0) / dx); x = 0; }

        if (out == outcode0) {
            *x0 = x; *y0 = y;
            outcode0 = INSIDE;
            if (*x0 < 0) outcode0 |= LEFT;
            else if (*x0 >= w) outcode0 |= RIGHT;
            if (*y0 < 0) outcode0 |= TOP;
            else if (*y0 >= h) outcode0 |= BOTTOM;
        } else {
            *x1 = x; *y1 = y;
            outcode1 = INSIDE;
            if (*x1 < 0) outcode1 |= LEFT;
            else if (*x1 >= w) outcode1 |= RIGHT;
            if (*y1 < 0) outcode1 |= TOP;
            else if (*y1 >= h) outcode1 |= BOTTOM;
        }
    }
}

pxl8_cpu_backend* pxl8_cpu_create(u32 width, u32 height) {
    pxl8_cpu_backend* cpu = calloc(1, sizeof(pxl8_cpu_backend));
    if (!cpu) return NULL;

    pxl8_cpu_render_target_desc desc = {
        .width = width,
        .height = height,
        .with_depth = true,
        .with_lighting = true,
    };
    pxl8_cpu_render_target* base_target = pxl8_cpu_create_render_target(&desc);
    if (!base_target) {
        free(cpu);
        return NULL;
    }

    cpu->target_stack[0] = base_target;
    cpu->target_stack_depth = 1;
    cpu->current_target = base_target;

    cpu->output_size = width * height;
    cpu->output = calloc(cpu->output_size, sizeof(u32));
    if (!cpu->output) {
        pxl8_cpu_destroy_render_target(base_target);
        free(cpu);
        return NULL;
    }

    return cpu;
}

void pxl8_cpu_destroy(pxl8_cpu_backend* cpu) {
    if (!cpu) return;
    for (u32 i = 0; i < cpu->target_stack_depth; i++) {
        pxl8_cpu_destroy_render_target(cpu->target_stack[i]);
    }
    free(cpu->output);
    free(cpu);
}

void pxl8_cpu_begin_frame(pxl8_cpu_backend* cpu, const pxl8_3d_frame* frame) {
    if (!cpu || !frame) return;
    cpu->frame = *frame;
    cpu->mvp = pxl8_mat4_mul(frame->projection, frame->view);
}

void pxl8_cpu_end_frame(pxl8_cpu_backend* cpu) {
    (void)cpu;
}

void pxl8_cpu_clear(pxl8_cpu_backend* cpu, u8 color) {
    if (!cpu || !cpu->current_target || !cpu->current_target->framebuffer) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;
    memset(render_target->framebuffer, color, render_target->width * render_target->height);
}

void pxl8_cpu_clear_depth(pxl8_cpu_backend* cpu) {
    if (!cpu || !cpu->current_target) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;
    u32 count = render_target->width * render_target->height;
    if (render_target->zbuffer) {
        memset(render_target->zbuffer, 0xFF, count * sizeof(u16));
    }
    if (render_target->light_accum) {
        memset(render_target->light_accum, 0, count * sizeof(u32));
    }
}

void pxl8_cpu_set_colormap(pxl8_cpu_backend* cpu, const pxl8_colormap* cm) {
    if (cpu) cpu->colormap = cm;
}

void pxl8_cpu_set_palette_cube(pxl8_cpu_backend* cpu, const pxl8_palette_cube* cube) {
    if (cpu) cpu->palette_cube = cube;
}

void pxl8_cpu_set_palette(pxl8_cpu_backend* cpu, const u32* palette) {
    if (cpu) cpu->palette = palette;
}

void pxl8_cpu_draw_pixel(pxl8_cpu_backend* cpu, i32 x, i32 y, u8 color) {
    if (!cpu || !cpu->current_target || !cpu->current_target->framebuffer) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;
    if (x < 0 || x >= (i32)render_target->width || y < 0 || y >= (i32)render_target->height) return;
    render_target->framebuffer[y * render_target->width + x] = color;
}

u8 pxl8_cpu_get_pixel(pxl8_cpu_backend* cpu, i32 x, i32 y) {
    if (!cpu || !cpu->current_target || !cpu->current_target->framebuffer) return 0;
    pxl8_cpu_render_target* render_target = cpu->current_target;
    if (x < 0 || x >= (i32)render_target->width || y < 0 || y >= (i32)render_target->height) return 0;
    return render_target->framebuffer[y * render_target->width + x];
}

void pxl8_cpu_draw_line_2d(pxl8_cpu_backend* cpu, i32 x0, i32 y0, i32 x1, i32 y1, u8 color) {
    if (!cpu || !cpu->current_target || !cpu->current_target->framebuffer) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;

    bool visible;
    clip_line_2d(&x0, &y0, &x1, &y1, (i32)render_target->width, (i32)render_target->height, &visible);
    if (!visible) return;

    i32 dx = x1 - x0;
    i32 dy = y1 - y0;
    i32 sx = dx > 0 ? 1 : -1;
    i32 sy = dy > 0 ? 1 : -1;
    dx = dx > 0 ? dx : -dx;
    dy = dy > 0 ? dy : -dy;

    i32 err = (dx > dy ? dx : -dy) / 2;
    u8* fb = render_target->framebuffer;
    u32 w = render_target->width;

    while (true) {
        fb[(u32)y0 * w + (u32)x0] = color;
        if (x0 == x1 && y0 == y1) break;
        i32 e2 = err;
        if (e2 > -dx) { err -= dy; x0 += sx; }
        if (e2 < dy) { err += dx; y0 += sy; }
    }
}

void pxl8_cpu_draw_rect(pxl8_cpu_backend* cpu, i32 x, i32 y, i32 w, i32 h, u8 color) {
    if (!cpu) return;
    pxl8_cpu_draw_line_2d(cpu, x, y, x + w - 1, y, color);
    pxl8_cpu_draw_line_2d(cpu, x + w - 1, y, x + w - 1, y + h - 1, color);
    pxl8_cpu_draw_line_2d(cpu, x + w - 1, y + h - 1, x, y + h - 1, color);
    pxl8_cpu_draw_line_2d(cpu, x, y + h - 1, x, y, color);
}

void pxl8_cpu_draw_rect_fill(pxl8_cpu_backend* cpu, i32 x, i32 y, i32 w, i32 h, u8 color) {
    if (!cpu || !cpu->current_target || !cpu->current_target->framebuffer) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;

    i32 x0 = (x < 0) ? 0 : x;
    i32 y0 = (y < 0) ? 0 : y;
    i32 x1 = (x + w > (i32)render_target->width) ? (i32)render_target->width : x + w;
    i32 y1 = (y + h > (i32)render_target->height) ? (i32)render_target->height : y + h;

    i32 rect_w = x1 - x0;
    if (rect_w <= 0 || y1 <= y0) return;

    for (i32 py = y0; py < y1; py++) {
        memset(render_target->framebuffer + py * render_target->width + x0, color, rect_w);
    }
}

void pxl8_cpu_draw_circle(pxl8_cpu_backend* cpu, i32 cx, i32 cy, i32 radius, u8 color) {
    if (!cpu) return;

    i32 x = radius;
    i32 y = 0;
    i32 err = 0;

    while (x >= y) {
        pxl8_cpu_draw_pixel(cpu, cx + x, cy + y, color);
        pxl8_cpu_draw_pixel(cpu, cx + y, cy + x, color);
        pxl8_cpu_draw_pixel(cpu, cx - y, cy + x, color);
        pxl8_cpu_draw_pixel(cpu, cx - x, cy + y, color);
        pxl8_cpu_draw_pixel(cpu, cx - x, cy - y, color);
        pxl8_cpu_draw_pixel(cpu, cx - y, cy - x, color);
        pxl8_cpu_draw_pixel(cpu, cx + y, cy - x, color);
        pxl8_cpu_draw_pixel(cpu, cx + x, cy - y, color);

        if (err <= 0) {
            y += 1;
            err += 2 * y + 1;
        } else {
            x -= 1;
            err -= 2 * x + 1;
        }
    }
}

void pxl8_cpu_draw_circle_fill(pxl8_cpu_backend* cpu, i32 cx, i32 cy, i32 radius, u8 color) {
    if (!cpu || !cpu->current_target || !cpu->current_target->framebuffer) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;

    i32 x0 = (cx - radius < 0) ? -cx : -radius;
    i32 y0 = (cy - radius < 0) ? -cy : -radius;
    i32 x1 = (cx + radius >= (i32)render_target->width) ? (i32)render_target->width - cx - 1 : radius;
    i32 y1 = (cy + radius >= (i32)render_target->height) ? (i32)render_target->height - cy - 1 : radius;

    for (i32 dy = y0; dy <= y1; dy++) {
        for (i32 dx = x0; dx <= x1; dx++) {
            if (dx * dx + dy * dy <= radius * radius) {
                render_target->framebuffer[(cy + dy) * render_target->width + (cx + dx)] = color;
            }
        }
    }
}

void pxl8_cpu_draw_line_3d(pxl8_cpu_backend* cpu, pxl8_vec3 v0, pxl8_vec3 v1, u8 color) {
    if (!cpu || !cpu->current_target) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;

    pxl8_vec4 c0 = pxl8_mat4_mul_vec4(cpu->mvp, (pxl8_vec4){v0.x, v0.y, v0.z, 1.0f});
    pxl8_vec4 c1 = pxl8_mat4_mul_vec4(cpu->mvp, (pxl8_vec4){v1.x, v1.y, v1.z, 1.0f});

    if (c0.w <= 0.0f || c1.w <= 0.0f) return;

    f32 hw = (f32)render_target->width * 0.5f;
    f32 hh = (f32)render_target->height * 0.5f;

    i32 x0 = (i32)(hw + c0.x / c0.w * hw);
    i32 y0 = (i32)(hh - c0.y / c0.w * hh);
    i32 x1 = (i32)(hw + c1.x / c1.w * hw);
    i32 y1 = (i32)(hh - c1.y / c1.w * hh);

    pxl8_cpu_draw_line_2d(cpu, x0, y0, x1, y1, color);
}

typedef struct {
    pxl8_vec4 clip_pos;
    pxl8_vec3 world_pos;
    pxl8_vec3 normal;
    f32 u, v;
    u8 color;
    u8 light;
    u32 light_color;
} vertex_output;

static inline u32 blend_tri_light_color(u32 lc0, u32 lc1, u32 lc2) {
    u32 a0 = (lc0 >> 24) & 0xFF;
    u32 a1 = (lc1 >> 24) & 0xFF;
    u32 a2 = (lc2 >> 24) & 0xFF;
    u32 total_a = a0 + a1 + a2;
    if (total_a == 0) return 0;

    u32 r = ((lc0 & 0xFF) * a0 + (lc1 & 0xFF) * a1 + (lc2 & 0xFF) * a2) / total_a;
    u32 g = (((lc0 >> 8) & 0xFF) * a0 + ((lc1 >> 8) & 0xFF) * a1 + ((lc2 >> 8) & 0xFF) * a2) / total_a;
    u32 b = (((lc0 >> 16) & 0xFF) * a0 + ((lc1 >> 16) & 0xFF) * a1 + ((lc2 >> 16) & 0xFF) * a2) / total_a;
    u32 a = total_a / 3;
    if (a > 255) a = 255;

    return r | (g << 8) | (b << 16) | (a << 24);
}

static inline u32 lerp_light_color(u32 a, u32 b, f32 t) {
    u32 ar = a & 0xFF;
    u32 ag = (a >> 8) & 0xFF;
    u32 ab = (a >> 16) & 0xFF;
    u32 aa = (a >> 24) & 0xFF;

    u32 br = b & 0xFF;
    u32 bg = (b >> 8) & 0xFF;
    u32 bb = (b >> 16) & 0xFF;
    u32 ba = (b >> 24) & 0xFF;

    u32 or = ar + (u32)((i32)(br - ar) * t);
    u32 og = ag + (u32)((i32)(bg - ag) * t);
    u32 ob = ab + (u32)((i32)(bb - ab) * t);
    u32 oa = aa + (u32)((i32)(ba - aa) * t);

    return or | (og << 8) | (ob << 16) | (oa << 24);
}

static vertex_output lerp_vertex(const vertex_output* a, const vertex_output* b, f32 t) {
    vertex_output out;
    out.clip_pos.x = a->clip_pos.x + (b->clip_pos.x - a->clip_pos.x) * t;
    out.clip_pos.y = a->clip_pos.y + (b->clip_pos.y - a->clip_pos.y) * t;
    out.clip_pos.z = a->clip_pos.z + (b->clip_pos.z - a->clip_pos.z) * t;
    out.clip_pos.w = a->clip_pos.w + (b->clip_pos.w - a->clip_pos.w) * t;
    out.world_pos.x = a->world_pos.x + (b->world_pos.x - a->world_pos.x) * t;
    out.world_pos.y = a->world_pos.y + (b->world_pos.y - a->world_pos.y) * t;
    out.world_pos.z = a->world_pos.z + (b->world_pos.z - a->world_pos.z) * t;
    out.normal.x = a->normal.x + (b->normal.x - a->normal.x) * t;
    out.normal.y = a->normal.y + (b->normal.y - a->normal.y) * t;
    out.normal.z = a->normal.z + (b->normal.z - a->normal.z) * t;
    out.u = a->u + (b->u - a->u) * t;
    out.v = a->v + (b->v - a->v) * t;
    out.color = t < 0.5f ? a->color : b->color;
    out.light = (u8)(a->light + (b->light - a->light) * t);
    out.light_color = lerp_light_color(a->light_color, b->light_color, t);
    return out;
}

static i32 clip_triangle_near(
    const vertex_output* v0, const vertex_output* v1, const vertex_output* v2,
    f32 near, vertex_output out[6]
) {
    bool in0 = v0->clip_pos.w >= near;
    bool in1 = v1->clip_pos.w >= near;
    bool in2 = v2->clip_pos.w >= near;
    i32 count = in0 + in1 + in2;

    if (count == 0) return 0;
    if (count == 3) {
        out[0] = *v0; out[1] = *v1; out[2] = *v2;
        return 3;
    }

    if (count == 1) {
        const vertex_output *inside, *out_a, *out_b;
        if (in0) { inside = v0; out_a = v1; out_b = v2; }
        else if (in1) { inside = v1; out_a = v2; out_b = v0; }
        else { inside = v2; out_a = v0; out_b = v1; }

        f32 t_a = (near - out_a->clip_pos.w) / (inside->clip_pos.w - out_a->clip_pos.w);
        f32 t_b = (near - out_b->clip_pos.w) / (inside->clip_pos.w - out_b->clip_pos.w);

        out[0] = *inside;
        out[1] = lerp_vertex(out_a, inside, t_a);
        out[2] = lerp_vertex(out_b, inside, t_b);
        return 3;
    }

    const vertex_output *outside, *in_a, *in_b;
    if (!in0) { outside = v0; in_a = v1; in_b = v2; }
    else if (!in1) { outside = v1; in_a = v2; in_b = v0; }
    else { outside = v2; in_a = v0; in_b = v1; }

    f32 t_a = (near - outside->clip_pos.w) / (in_a->clip_pos.w - outside->clip_pos.w);
    f32 t_b = (near - outside->clip_pos.w) / (in_b->clip_pos.w - outside->clip_pos.w);

    vertex_output new_a = lerp_vertex(outside, in_a, t_a);
    vertex_output new_b = lerp_vertex(outside, in_b, t_b);

    out[0] = *in_a; out[1] = *in_b; out[2] = new_b;
    out[3] = *in_a; out[4] = new_b; out[5] = new_a;
    return 6;
}

typedef struct {
    i32 x0, y0, x1, y1, x2, y2;
    f32 z0, z1, z2;
    f32 w0_recip, w1_recip, w2_recip;
    f32 u0_w, v0_w, u1_w, v1_w, u2_w, v2_w;
    f32 l0_w, l1_w, l2_w;
    u32 lc0, lc1, lc2;
    f32 c0_w, c1_w, c2_w;
    i32 y_start, y_end, total_height;
    f32 inv_total;
    u32 target_width, target_height;
} tri_setup;

static bool setup_triangle(
    tri_setup* setup,
    const vertex_output* vo0, const vertex_output* vo1, const vertex_output* vo2,
    u32 width, u32 height, bool double_sided
) {
    f32 hw = (f32)width * 0.5f;
    f32 hh = (f32)height * 0.5f;

    setup->x0 = (i32)(hw + vo0->clip_pos.x / vo0->clip_pos.w * hw);
    setup->y0 = (i32)(hh - vo0->clip_pos.y / vo0->clip_pos.w * hh);
    setup->z0 = vo0->clip_pos.z / vo0->clip_pos.w;

    setup->x1 = (i32)(hw + vo1->clip_pos.x / vo1->clip_pos.w * hw);
    setup->y1 = (i32)(hh - vo1->clip_pos.y / vo1->clip_pos.w * hh);
    setup->z1 = vo1->clip_pos.z / vo1->clip_pos.w;

    setup->x2 = (i32)(hw + vo2->clip_pos.x / vo2->clip_pos.w * hw);
    setup->y2 = (i32)(hh - vo2->clip_pos.y / vo2->clip_pos.w * hh);
    setup->z2 = vo2->clip_pos.z / vo2->clip_pos.w;

    i32 cross = (setup->x1 - setup->x0) * (setup->y2 - setup->y0) -
                (setup->y1 - setup->y0) * (setup->x2 - setup->x0);
    if (!double_sided && cross >= 0) return false;

    const vertex_output* sorted[3] = {vo0, vo1, vo2};

    if (setup->y0 > setup->y1) {
        i32 t = setup->x0; setup->x0 = setup->x1; setup->x1 = t;
        t = setup->y0; setup->y0 = setup->y1; setup->y1 = t;
        f32 tz = setup->z0; setup->z0 = setup->z1; setup->z1 = tz;
        const vertex_output* tv = sorted[0]; sorted[0] = sorted[1]; sorted[1] = tv;
    }
    if (setup->y0 > setup->y2) {
        i32 t = setup->x0; setup->x0 = setup->x2; setup->x2 = t;
        t = setup->y0; setup->y0 = setup->y2; setup->y2 = t;
        f32 tz = setup->z0; setup->z0 = setup->z2; setup->z2 = tz;
        const vertex_output* tv = sorted[0]; sorted[0] = sorted[2]; sorted[2] = tv;
    }
    if (setup->y1 > setup->y2) {
        i32 t = setup->x1; setup->x1 = setup->x2; setup->x2 = t;
        t = setup->y1; setup->y1 = setup->y2; setup->y2 = t;
        f32 tz = setup->z1; setup->z1 = setup->z2; setup->z2 = tz;
        const vertex_output* tv = sorted[1]; sorted[1] = sorted[2]; sorted[2] = tv;
    }

    setup->total_height = setup->y2 - setup->y0;
    if (setup->total_height == 0) return false;

    setup->y_start = setup->y0 < 0 ? 0 : setup->y0;
    setup->y_end = setup->y2 >= (i32)height ? (i32)height - 1 : setup->y2;

    setup->w0_recip = 1.0f / sorted[0]->clip_pos.w;
    setup->w1_recip = 1.0f / sorted[1]->clip_pos.w;
    setup->w2_recip = 1.0f / sorted[2]->clip_pos.w;

    setup->u0_w = sorted[0]->u * setup->w0_recip;
    setup->v0_w = sorted[0]->v * setup->w0_recip;
    setup->u1_w = sorted[1]->u * setup->w1_recip;
    setup->v1_w = sorted[1]->v * setup->w1_recip;
    setup->u2_w = sorted[2]->u * setup->w2_recip;
    setup->v2_w = sorted[2]->v * setup->w2_recip;

    setup->l0_w = sorted[0]->light * setup->w0_recip;
    setup->l1_w = sorted[1]->light * setup->w1_recip;
    setup->l2_w = sorted[2]->light * setup->w2_recip;

    setup->lc0 = sorted[0]->light_color;
    setup->lc1 = sorted[1]->light_color;
    setup->lc2 = sorted[2]->light_color;

    setup->c0_w = (f32)sorted[0]->color * setup->w0_recip;
    setup->c1_w = (f32)sorted[1]->color * setup->w1_recip;
    setup->c2_w = (f32)sorted[2]->color * setup->w2_recip;

    setup->inv_total = 1.0f / (f32)setup->total_height;
    setup->target_width = width;
    setup->target_height = height;

    return true;
}

static void rasterize_triangle_opaque(
    pxl8_cpu_backend* cpu,
    const tri_setup* setup,
    const pxl8_atlas* textures, u32 texture_id, bool dither
) {
    pxl8_cpu_render_target* render_target = cpu->current_target;
    u32* light_accum = render_target->light_accum;
    u32 tri_light_color = light_accum ? blend_tri_light_color(setup->lc0, setup->lc1, setup->lc2) : 0;
    const pxl8_atlas_entry* tex_entry = textures ? pxl8_atlas_get_entry(textures, texture_id) : NULL;
    u32 atlas_width = textures ? pxl8_atlas_get_width(textures) : 1;
    f32 tex_w = tex_entry ? (f32)tex_entry->w : 1.0f;
    f32 tex_h = tex_entry ? (f32)tex_entry->h : 1.0f;
    i32 tex_stride = (i32)atlas_width;
    i32 tex_mask_w = tex_entry ? (i32)tex_entry->w - 1 : 0;
    i32 tex_mask_h = tex_entry ? (i32)tex_entry->h - 1 : 0;
    u32 tex_base = tex_entry ? (u32)tex_entry->y * atlas_width + (u32)tex_entry->x : 0;
    const u8* tex_pixels = textures ? pxl8_atlas_get_pixels(textures) : NULL;

    for (i32 y = setup->y_start; y <= setup->y_end; y++) {
        bool second_half = y > setup->y1 || setup->y1 == setup->y0;
        i32 segment_height = second_half ? setup->y2 - setup->y1 : setup->y1 - setup->y0;
        if (segment_height == 0) continue;

        f32 alpha = (f32)(y - setup->y0) * setup->inv_total;
        f32 beta = (f32)(y - (second_half ? setup->y1 : setup->y0)) / (f32)segment_height;

        f32 ax = (f32)setup->x0 + (f32)(setup->x2 - setup->x0) * alpha;
        f32 az = setup->z0 + (setup->z2 - setup->z0) * alpha;
        f32 a_wr = setup->w0_recip + (setup->w2_recip - setup->w0_recip) * alpha;
        f32 a_uw = setup->u0_w + (setup->u2_w - setup->u0_w) * alpha;
        f32 a_vw = setup->v0_w + (setup->v2_w - setup->v0_w) * alpha;
        f32 a_lw = setup->l0_w + (setup->l2_w - setup->l0_w) * alpha;

        f32 bx, bz, b_wr, b_uw, b_vw, b_lw;
        if (second_half) {
            bx = (f32)setup->x1 + (f32)(setup->x2 - setup->x1) * beta;
            bz = setup->z1 + (setup->z2 - setup->z1) * beta;
            b_wr = setup->w1_recip + (setup->w2_recip - setup->w1_recip) * beta;
            b_uw = setup->u1_w + (setup->u2_w - setup->u1_w) * beta;
            b_vw = setup->v1_w + (setup->v2_w - setup->v1_w) * beta;
            b_lw = setup->l1_w + (setup->l2_w - setup->l1_w) * beta;
        } else {
            bx = (f32)setup->x0 + (f32)(setup->x1 - setup->x0) * beta;
            bz = setup->z0 + (setup->z1 - setup->z0) * beta;
            b_wr = setup->w0_recip + (setup->w1_recip - setup->w0_recip) * beta;
            b_uw = setup->u0_w + (setup->u1_w - setup->u0_w) * beta;
            b_vw = setup->v0_w + (setup->v1_w - setup->v0_w) * beta;
            b_lw = setup->l0_w + (setup->l1_w - setup->l0_w) * beta;
        }

        f32 x_start_f, x_end_f, z_start, z_end;
        f32 wr_start, wr_end, uw_start, uw_end, vw_start, vw_end, lw_start, lw_end;

        if (ax <= bx) {
            x_start_f = ax; x_end_f = bx; z_start = az; z_end = bz;
            wr_start = a_wr; wr_end = b_wr;
            uw_start = a_uw; uw_end = b_uw;
            vw_start = a_vw; vw_end = b_vw;
            lw_start = a_lw; lw_end = b_lw;
        } else {
            x_start_f = bx; x_end_f = ax; z_start = bz; z_end = az;
            wr_start = b_wr; wr_end = a_wr;
            uw_start = b_uw; uw_end = a_uw;
            vw_start = b_vw; vw_end = a_vw;
            lw_start = b_lw; lw_end = a_lw;
        }

        i32 x_start_orig = (i32)(x_start_f + 0.5f);
        i32 x_end_orig = (i32)(x_end_f + 0.5f) - 1;
        i32 x_start = x_start_orig < 0 ? 0 : x_start_orig;
        i32 x_end = x_end_orig >= (i32)setup->target_width ? (i32)setup->target_width - 1 : x_end_orig;
        if (x_start > x_end) continue;

        i32 width_orig = x_end_orig - x_start_orig;
        if (width_orig < 1) width_orig = 1;
        f32 inv_width = 1.0f / (f32)width_orig;

        f32 dz = (z_end - z_start) * inv_width;
        f32 dwr = (wr_end - wr_start) * inv_width;
        f32 duw = (uw_end - uw_start) * inv_width;
        f32 dvw = (vw_end - vw_start) * inv_width;
        f32 d_lw = (lw_end - lw_start) * inv_width;

        f32 skip = (f32)(x_start - x_start_orig);
        f32 z = z_start + dz * skip;
        f32 wr = wr_start + dwr * skip;
        f32 uw = uw_start + duw * skip;
        f32 vw = vw_start + dvw * skip;
        f32 lw = lw_start + d_lw * skip;

        u32 row_start = (u32)y * render_target->width;
        u8* prow = render_target->framebuffer + row_start;
        u16* zrow = render_target->zbuffer + row_start;

        const i32 SUBDIV = 32;
        i32 x = x_start;

        while (x <= x_end) {
            i32 span_end = x + SUBDIV - 1;
            if (span_end > x_end) span_end = x_end;
            i32 span_len = span_end - x + 1;
            f32 steps = (f32)span_len;

            f32 pw_start = 1.0f / wr;
            f32 pw_end = 1.0f / (wr + dwr * steps);

            f32 u_start = uw * pw_start;
            f32 v_start = vw * pw_start;
            f32 u_end = (uw + duw * steps) * pw_end;
            f32 v_end = (vw + dvw * steps) * pw_end;
            f32 l_start = lw * pw_start;
            f32 l_end = (lw + d_lw * steps) * pw_end;

            f32 fog_density = cpu->frame.uniforms.fog_density;
            if (fog_density > 0.0f) {
                f32 z_end_fog = z + dz * steps;
                f32 t_start = (z + 1.0f) * 0.5f;
                f32 t_end = (z_end_fog + 1.0f) * 0.5f;
                if (t_start < 0.0f) t_start = 0.0f;
                if (t_end < 0.0f) t_end = 0.0f;
                f32 fog_start = t_start * t_start * fog_density;
                f32 fog_end = t_end * t_end * fog_density;
                if (fog_start > 1.0f) fog_start = 1.0f;
                if (fog_end > 1.0f) fog_end = 1.0f;
                l_start *= (1.0f - fog_start);
                l_end *= (1.0f - fog_end);
            }

            if (l_start > 255.0f) l_start = 255.0f;
            if (l_end > 255.0f) l_end = 255.0f;

            f32 inv_span = span_len > 1 ? 1.0f / (f32)(span_len - 1) : 0.0f;
            f32 du = (u_end - u_start) * inv_span;
            f32 dv = (v_end - v_start) * inv_span;
            f32 dl = (l_end - l_start) * inv_span;

            i32 u_fixed = (i32)(u_start * tex_w * 65536.0f);
            i32 v_fixed = (i32)(v_start * tex_h * 65536.0f);
            i32 du_fixed = (i32)(du * tex_w * 65536.0f);
            i32 dv_fixed = (i32)(dv * tex_h * 65536.0f);

            f32 l_a = l_start;
            f32 z_a = z;

            for (i32 px = x; px <= span_end; px++) {
                u16 z16 = depth_to_u16(z_a);
                if (z16 < zrow[px]) {
                    i32 tx = (u_fixed >> 16) & tex_mask_w;
                    i32 ty = (v_fixed >> 16) & tex_mask_h;
                    u8 tex_idx = tex_pixels ? tex_pixels[tex_base + (u32)ty * (u32)tex_stride + (u32)tx] : 1;

                    if (tex_idx != 0) {
                        u8 light = (u8)l_a;
                        if (dither) {
                            light = pxl8_dither_light(light, (u32)px, (u32)y);
                        }
                        u8 pal_idx = cpu->colormap ? pxl8_colormap_lookup(cpu->colormap, tex_idx, PXL8_LIGHT_WHITE, light) : tex_idx;
                        prow[px] = pal_idx;
                        zrow[px] = z16;
                        if (light_accum) {
                            light_accum[row_start + (u32)px] = tri_light_color;
                        }
                    }
                }

                u_fixed += du_fixed;
                v_fixed += dv_fixed;
                l_a += dl;
                z_a += dz;
            }

            wr += dwr * steps;
            uw += duw * steps;
            vw += dvw * steps;
            lw += d_lw * steps;
            z += dz * steps;
            x = span_end + 1;
        }
    }
}

static void rasterize_triangle_passthrough(
    pxl8_cpu_backend* cpu,
    const tri_setup* setup
) {
    pxl8_cpu_render_target* render_target = cpu->current_target;

    for (i32 y = setup->y_start; y <= setup->y_end; y++) {
        bool second_half = y > setup->y1 || setup->y1 == setup->y0;
        i32 segment_height = second_half ? setup->y2 - setup->y1 : setup->y1 - setup->y0;
        if (segment_height == 0) continue;

        f32 alpha = (f32)(y - setup->y0) * setup->inv_total;
        f32 beta = (f32)(y - (second_half ? setup->y1 : setup->y0)) / (f32)segment_height;

        f32 ax = (f32)setup->x0 + (f32)(setup->x2 - setup->x0) * alpha;
        f32 az = setup->z0 + (setup->z2 - setup->z0) * alpha;
        f32 a_wr = setup->w0_recip + (setup->w2_recip - setup->w0_recip) * alpha;
        f32 a_cw = setup->c0_w + (setup->c2_w - setup->c0_w) * alpha;

        f32 bx, bz, b_wr, b_cw;
        if (second_half) {
            bx = (f32)setup->x1 + (f32)(setup->x2 - setup->x1) * beta;
            bz = setup->z1 + (setup->z2 - setup->z1) * beta;
            b_wr = setup->w1_recip + (setup->w2_recip - setup->w1_recip) * beta;
            b_cw = setup->c1_w + (setup->c2_w - setup->c1_w) * beta;
        } else {
            bx = (f32)setup->x0 + (f32)(setup->x1 - setup->x0) * beta;
            bz = setup->z0 + (setup->z1 - setup->z0) * beta;
            b_wr = setup->w0_recip + (setup->w1_recip - setup->w0_recip) * beta;
            b_cw = setup->c0_w + (setup->c1_w - setup->c0_w) * beta;
        }

        f32 x_start_f, x_end_f, z_start, z_end, wr_start, wr_end, cw_start, cw_end;
        if (ax <= bx) {
            x_start_f = ax; x_end_f = bx;
            z_start = az; z_end = bz;
            wr_start = a_wr; wr_end = b_wr;
            cw_start = a_cw; cw_end = b_cw;
        } else {
            x_start_f = bx; x_end_f = ax;
            z_start = bz; z_end = az;
            wr_start = b_wr; wr_end = a_wr;
            cw_start = b_cw; cw_end = a_cw;
        }

        i32 x_start_orig = (i32)(x_start_f + 0.5f);
        i32 x_end_orig = (i32)(x_end_f + 0.5f) - 1;
        i32 x_start = x_start_orig < 0 ? 0 : x_start_orig;
        i32 x_end = x_end_orig >= (i32)setup->target_width ? (i32)setup->target_width - 1 : x_end_orig;
        if (x_start > x_end) continue;

        i32 width_orig = x_end_orig - x_start_orig;
        if (width_orig < 1) width_orig = 1;
        f32 inv_width = 1.0f / (f32)width_orig;

        f32 dz = (z_end - z_start) * inv_width;
        f32 dwr = (wr_end - wr_start) * inv_width;
        f32 dcw = (cw_end - cw_start) * inv_width;

        f32 skip = (f32)(x_start - x_start_orig);
        f32 z = z_start + dz * skip;
        f32 wr = wr_start + dwr * skip;
        f32 cw = cw_start + dcw * skip;

        u32 row_start = (u32)y * render_target->width;
        u8* prow = render_target->framebuffer + row_start;
        u16* zrow = render_target->zbuffer + row_start;

        for (i32 px = x_start; px <= x_end; px++) {
            u16 z16 = depth_to_u16(z);
            if (z16 < zrow[px]) {
                f32 w = 1.0f / wr;
                f32 color_f = cw * w;
                u8 color = pxl8_dither_float(color_f, (u32)px, (u32)y);
                prow[px] = color;
                zrow[px] = z16;
            }
            z += dz;
            wr += dwr;
            cw += dcw;
        }
    }
}

static void rasterize_triangle_alpha(
    pxl8_cpu_backend* cpu,
    const tri_setup* setup,
    const pxl8_atlas* textures, u32 texture_id, u8 mat_alpha, bool dither
) {
    pxl8_cpu_render_target* render_target = cpu->current_target;
    u32* light_accum = render_target->light_accum;
    u32 tri_light_color = light_accum ? blend_tri_light_color(setup->lc0, setup->lc1, setup->lc2) : 0;
    const pxl8_atlas_entry* tex_entry = textures ? pxl8_atlas_get_entry(textures, texture_id) : NULL;
    u32 atlas_width = textures ? pxl8_atlas_get_width(textures) : 1;
    f32 tex_w = tex_entry ? (f32)tex_entry->w : 1.0f;
    f32 tex_h = tex_entry ? (f32)tex_entry->h : 1.0f;
    i32 tex_stride = (i32)atlas_width;
    i32 tex_mask_w = tex_entry ? (i32)tex_entry->w - 1 : 0;
    i32 tex_mask_h = tex_entry ? (i32)tex_entry->h - 1 : 0;
    u32 tex_base = tex_entry ? (u32)tex_entry->y * atlas_width + (u32)tex_entry->x : 0;
    const u8* tex_pixels = textures ? pxl8_atlas_get_pixels(textures) : NULL;

    for (i32 y = setup->y_start; y <= setup->y_end; y++) {
        bool second_half = y > setup->y1 || setup->y1 == setup->y0;
        i32 segment_height = second_half ? setup->y2 - setup->y1 : setup->y1 - setup->y0;
        if (segment_height == 0) continue;

        f32 alpha = (f32)(y - setup->y0) * setup->inv_total;
        f32 beta = (f32)(y - (second_half ? setup->y1 : setup->y0)) / (f32)segment_height;

        f32 ax = (f32)setup->x0 + (f32)(setup->x2 - setup->x0) * alpha;
        f32 az = setup->z0 + (setup->z2 - setup->z0) * alpha;
        f32 a_wr = setup->w0_recip + (setup->w2_recip - setup->w0_recip) * alpha;
        f32 a_uw = setup->u0_w + (setup->u2_w - setup->u0_w) * alpha;
        f32 a_vw = setup->v0_w + (setup->v2_w - setup->v0_w) * alpha;
        f32 a_lw = setup->l0_w + (setup->l2_w - setup->l0_w) * alpha;

        f32 bx, bz, b_wr, b_uw, b_vw, b_lw;
        if (second_half) {
            bx = (f32)setup->x1 + (f32)(setup->x2 - setup->x1) * beta;
            bz = setup->z1 + (setup->z2 - setup->z1) * beta;
            b_wr = setup->w1_recip + (setup->w2_recip - setup->w1_recip) * beta;
            b_uw = setup->u1_w + (setup->u2_w - setup->u1_w) * beta;
            b_vw = setup->v1_w + (setup->v2_w - setup->v1_w) * beta;
            b_lw = setup->l1_w + (setup->l2_w - setup->l1_w) * beta;
        } else {
            bx = (f32)setup->x0 + (f32)(setup->x1 - setup->x0) * beta;
            bz = setup->z0 + (setup->z1 - setup->z0) * beta;
            b_wr = setup->w0_recip + (setup->w1_recip - setup->w0_recip) * beta;
            b_uw = setup->u0_w + (setup->u1_w - setup->u0_w) * beta;
            b_vw = setup->v0_w + (setup->v1_w - setup->v0_w) * beta;
            b_lw = setup->l0_w + (setup->l1_w - setup->l0_w) * beta;
        }

        f32 x_start_f, x_end_f, z_start, z_end;
        f32 wr_start, wr_end, uw_start, uw_end, vw_start, vw_end, lw_start, lw_end;

        if (ax <= bx) {
            x_start_f = ax; x_end_f = bx; z_start = az; z_end = bz;
            wr_start = a_wr; wr_end = b_wr;
            uw_start = a_uw; uw_end = b_uw;
            vw_start = a_vw; vw_end = b_vw;
            lw_start = a_lw; lw_end = b_lw;
        } else {
            x_start_f = bx; x_end_f = ax; z_start = bz; z_end = az;
            wr_start = b_wr; wr_end = a_wr;
            uw_start = b_uw; uw_end = a_uw;
            vw_start = b_vw; vw_end = a_vw;
            lw_start = b_lw; lw_end = a_lw;
        }

        i32 x_start_orig = (i32)(x_start_f + 0.5f);
        i32 x_end_orig = (i32)(x_end_f + 0.5f) - 1;
        i32 x_start = x_start_orig < 0 ? 0 : x_start_orig;
        i32 x_end = x_end_orig >= (i32)setup->target_width ? (i32)setup->target_width - 1 : x_end_orig;
        if (x_start > x_end) continue;

        i32 width_orig = x_end_orig - x_start_orig;
        if (width_orig < 1) width_orig = 1;
        f32 inv_width = 1.0f / (f32)width_orig;

        f32 dz = (z_end - z_start) * inv_width;
        f32 dwr = (wr_end - wr_start) * inv_width;
        f32 duw = (uw_end - uw_start) * inv_width;
        f32 dvw = (vw_end - vw_start) * inv_width;
        f32 d_lw = (lw_end - lw_start) * inv_width;

        f32 skip = (f32)(x_start - x_start_orig);
        f32 z = z_start + dz * skip;
        f32 wr = wr_start + dwr * skip;
        f32 uw = uw_start + duw * skip;
        f32 vw = vw_start + dvw * skip;
        f32 lw = lw_start + d_lw * skip;

        u32 row_start = (u32)y * render_target->width;
        u8* prow = render_target->framebuffer + row_start;
        u16* zrow = render_target->zbuffer + row_start;

        const i32 SUBDIV = 32;
        i32 x = x_start;

        while (x <= x_end) {
            i32 span_end = x + SUBDIV - 1;
            if (span_end > x_end) span_end = x_end;
            i32 span_len = span_end - x + 1;
            f32 steps = (f32)span_len;

            f32 pw_start = 1.0f / wr;
            f32 pw_end = 1.0f / (wr + dwr * steps);

            f32 u_start = uw * pw_start;
            f32 v_start = vw * pw_start;
            f32 u_end = (uw + duw * steps) * pw_end;
            f32 v_end = (vw + dvw * steps) * pw_end;
            f32 l_start = lw * pw_start;
            f32 l_end = (lw + d_lw * steps) * pw_end;

            f32 fog_density = cpu->frame.uniforms.fog_density;
            if (fog_density > 0.0f) {
                f32 z_end_fog = z + dz * steps;
                f32 t_start = (z + 1.0f) * 0.5f;
                f32 t_end = (z_end_fog + 1.0f) * 0.5f;
                if (t_start < 0.0f) t_start = 0.0f;
                if (t_end < 0.0f) t_end = 0.0f;
                f32 fog_start = t_start * t_start * fog_density;
                f32 fog_end = t_end * t_end * fog_density;
                if (fog_start > 1.0f) fog_start = 1.0f;
                if (fog_end > 1.0f) fog_end = 1.0f;
                l_start *= (1.0f - fog_start);
                l_end *= (1.0f - fog_end);
            }

            if (l_start > 255.0f) l_start = 255.0f;
            if (l_end > 255.0f) l_end = 255.0f;

            f32 inv_span = span_len > 1 ? 1.0f / (f32)(span_len - 1) : 0.0f;
            f32 du = (u_end - u_start) * inv_span;
            f32 dv = (v_end - v_start) * inv_span;
            f32 dl = (l_end - l_start) * inv_span;

            i32 u_fixed = (i32)(u_start * tex_w * 65536.0f);
            i32 v_fixed = (i32)(v_start * tex_h * 65536.0f);
            i32 du_fixed = (i32)(du * tex_w * 65536.0f);
            i32 dv_fixed = (i32)(dv * tex_h * 65536.0f);

            f32 l_a = l_start;
            f32 z_a = z;

            for (i32 px = x; px <= span_end; px++) {
                i32 tx = (u_fixed >> 16) & tex_mask_w;
                i32 ty = (v_fixed >> 16) & tex_mask_h;
                u8 tex_idx = tex_pixels ? tex_pixels[tex_base + (u32)ty * (u32)tex_stride + (u32)tx] : 1;

                if (tex_idx != 0) {
                    u8 light = (u8)l_a;
                    if (dither) {
                        light = pxl8_dither_light(light, (u32)px, (u32)y);
                    }
                    u8 src_idx = cpu->colormap ? pxl8_colormap_lookup(cpu->colormap, tex_idx, PXL8_LIGHT_WHITE, light) : tex_idx;

                    if (mat_alpha >= 128) {
                        prow[px] = src_idx;
                        u16 z16 = depth_to_u16(z_a);
                        if (z16 < zrow[px]) zrow[px] = z16;
                        if (light_accum) {
                            light_accum[row_start + (u32)px] = tri_light_color;
                        }
                    }
                }

                u_fixed += du_fixed;
                v_fixed += dv_fixed;
                l_a += dl;
                z_a += dz;
            }

            wr += dwr * steps;
            uw += duw * steps;
            vw += dvw * steps;
            lw += d_lw * steps;
            z += dz * steps;
            x = span_end + 1;
        }
    }
}

static void rasterize_triangle_wireframe(
    pxl8_cpu_backend* cpu,
    const vertex_output* vo0, const vertex_output* vo1, const vertex_output* vo2,
    u8 color, bool double_sided
) {
    pxl8_cpu_render_target* render_target = cpu->current_target;
    f32 hw = (f32)render_target->width * 0.5f;
    f32 hh = (f32)render_target->height * 0.5f;

    i32 x0 = (i32)(hw + vo0->clip_pos.x / vo0->clip_pos.w * hw);
    i32 y0 = (i32)(hh - vo0->clip_pos.y / vo0->clip_pos.w * hh);
    i32 x1 = (i32)(hw + vo1->clip_pos.x / vo1->clip_pos.w * hw);
    i32 y1 = (i32)(hh - vo1->clip_pos.y / vo1->clip_pos.w * hh);
    i32 x2 = (i32)(hw + vo2->clip_pos.x / vo2->clip_pos.w * hw);
    i32 y2 = (i32)(hh - vo2->clip_pos.y / vo2->clip_pos.w * hh);

    if (!double_sided) {
        i32 cross = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0);
        if (cross >= 0) return;
    }

    pxl8_cpu_draw_line_2d(cpu, x0, y0, x1, y1, color);
    pxl8_cpu_draw_line_2d(cpu, x1, y1, x2, y2, color);
    pxl8_cpu_draw_line_2d(cpu, x2, y2, x0, y0, color);
}

static void dispatch_triangle(
    pxl8_cpu_backend* cpu,
    const vertex_output* vo0, const vertex_output* vo1, const vertex_output* vo2,
    const pxl8_atlas* textures, const pxl8_gfx_material* material
) {
    if (material->wireframe) {
        u8 color = (material->texture_id > 0) ? (u8)material->texture_id : 15;
        rasterize_triangle_wireframe(cpu, vo0, vo1, vo2, color, material->double_sided);
        return;
    }

    pxl8_cpu_render_target* render_target = cpu->current_target;
    tri_setup setup;
    if (!setup_triangle(&setup, vo0, vo1, vo2, render_target->width, render_target->height, material->double_sided)) {
        return;
    }

    bool alpha_blend = material->alpha < 255;
    bool passthrough = material->vertex_color_passthrough;

    if (alpha_blend) {
        rasterize_triangle_alpha(cpu, &setup, textures, material->texture_id, material->alpha, material->dither);
    } else if (passthrough) {
        rasterize_triangle_passthrough(cpu, &setup);
    } else {
        rasterize_triangle_opaque(cpu, &setup, textures, material->texture_id, material->dither);
    }
}

void pxl8_cpu_draw_mesh(
    pxl8_cpu_backend* cpu,
    const pxl8_mesh* mesh,
    const pxl8_mat4* model,
    const pxl8_gfx_material* material,
    const pxl8_atlas* textures
) {
    if (!cpu || !mesh || !model || !material || mesh->index_count < 3 || !cpu->current_target) return;

    pxl8_mat4 mv = pxl8_mat4_mul(cpu->frame.view, *model);
    pxl8_mat4 mvp = pxl8_mat4_mul(cpu->frame.projection, mv);

    f32 near = cpu->frame.near_clip > 0.0f ? cpu->frame.near_clip : 0.1f;

    for (u32 i = 0; i < mesh->index_count; i += 3) {
        u16 i0 = mesh->indices[i];
        u16 i1 = mesh->indices[i + 1];
        u16 i2 = mesh->indices[i + 2];

        const pxl8_vertex* v0 = &mesh->vertices[i0];
        const pxl8_vertex* v1 = &mesh->vertices[i1];
        const pxl8_vertex* v2 = &mesh->vertices[i2];

        vertex_output vo0, vo1, vo2;

        pxl8_vec4 p0 = {v0->position.x, v0->position.y, v0->position.z, 1.0f};
        pxl8_vec4 p1 = {v1->position.x, v1->position.y, v1->position.z, 1.0f};
        pxl8_vec4 p2 = {v2->position.x, v2->position.y, v2->position.z, 1.0f};

        vo0.clip_pos = pxl8_mat4_mul_vec4(mvp, p0);
        vo1.clip_pos = pxl8_mat4_mul_vec4(mvp, p1);
        vo2.clip_pos = pxl8_mat4_mul_vec4(mvp, p2);

        pxl8_vec4 w0 = pxl8_mat4_mul_vec4(*model, p0);
        pxl8_vec4 w1 = pxl8_mat4_mul_vec4(*model, p1);
        pxl8_vec4 w2 = pxl8_mat4_mul_vec4(*model, p2);
        vo0.world_pos = (pxl8_vec3){w0.x, w0.y, w0.z};
        vo1.world_pos = (pxl8_vec3){w1.x, w1.y, w1.z};
        vo2.world_pos = (pxl8_vec3){w2.x, w2.y, w2.z};

        vo0.normal = v0->normal;
        vo1.normal = v1->normal;
        vo2.normal = v2->normal;

        vo0.u = v0->u; vo0.v = v0->v;
        vo1.u = v1->u; vo1.v = v1->v;
        vo2.u = v2->u; vo2.v = v2->v;

        vo0.color = v0->color;
        vo1.color = v1->color;
        vo2.color = v2->color;

        if (material->dynamic_lighting) {
            pxl8_vec3 n0 = pxl8_vec3_normalize(pxl8_mat4_mul_vec3(*model, v0->normal));
            pxl8_vec3 n1 = pxl8_vec3_normalize(pxl8_mat4_mul_vec3(*model, v1->normal));
            pxl8_vec3 n2 = pxl8_vec3_normalize(pxl8_mat4_mul_vec3(*model, v2->normal));

            pxl8_light_result lr0 = calc_vertex_light(vo0.world_pos, n0, &cpu->frame);
            pxl8_light_result lr1 = calc_vertex_light(vo1.world_pos, n1, &cpu->frame);
            pxl8_light_result lr2 = calc_vertex_light(vo2.world_pos, n2, &cpu->frame);

            vo0.light = lr0.light;
            vo1.light = lr1.light;
            vo2.light = lr2.light;

            vo0.light_color = lr0.light_color;
            vo1.light_color = lr1.light_color;
            vo2.light_color = lr2.light_color;
        } else {
            vo0.light = 255;
            vo1.light = 255;
            vo2.light = 255;
            vo0.light_color = 0;
            vo1.light_color = 0;
            vo2.light_color = 0;
        }

        vertex_output clipped[6];
        i32 clipped_count = clip_triangle_near(&vo0, &vo1, &vo2, near, clipped);

        for (i32 t = 0; t < clipped_count; t += 3) {
            dispatch_triangle(cpu, &clipped[t], &clipped[t+1], &clipped[t+2], textures, material);
        }
    }
}

void pxl8_cpu_draw_mesh_wireframe(
    pxl8_cpu_backend* cpu,
    const pxl8_mesh* mesh,
    pxl8_mat4 model,
    u8 color
) {
    if (!cpu || !cpu->current_target || !mesh || mesh->index_count < 3) return;
    pxl8_cpu_render_target* render_target = cpu->current_target;

    pxl8_mat4 mvp = pxl8_mat4_mul(cpu->frame.projection, pxl8_mat4_mul(cpu->frame.view, model));

    for (u32 i = 0; i < mesh->index_count; i += 3) {
        const pxl8_vertex* v0 = &mesh->vertices[mesh->indices[i]];
        const pxl8_vertex* v1 = &mesh->vertices[mesh->indices[i + 1]];
        const pxl8_vertex* v2 = &mesh->vertices[mesh->indices[i + 2]];

        pxl8_vec4 c0 = pxl8_mat4_mul_vec4(mvp, (pxl8_vec4){v0->position.x, v0->position.y, v0->position.z, 1.0f});
        pxl8_vec4 c1 = pxl8_mat4_mul_vec4(mvp, (pxl8_vec4){v1->position.x, v1->position.y, v1->position.z, 1.0f});
        pxl8_vec4 c2 = pxl8_mat4_mul_vec4(mvp, (pxl8_vec4){v2->position.x, v2->position.y, v2->position.z, 1.0f});

        if (c0.w <= 0.0f || c1.w <= 0.0f || c2.w <= 0.0f) continue;

        f32 hw = (f32)render_target->width * 0.5f;
        f32 hh = (f32)render_target->height * 0.5f;

        i32 x0 = (i32)(hw + c0.x / c0.w * hw);
        i32 y0 = (i32)(hh - c0.y / c0.w * hh);
        i32 x1 = (i32)(hw + c1.x / c1.w * hw);
        i32 y1 = (i32)(hh - c1.y / c1.w * hh);
        i32 x2 = (i32)(hw + c2.x / c2.w * hw);
        i32 y2 = (i32)(hh - c2.y / c2.w * hh);

        pxl8_cpu_draw_line_2d(cpu, x0, y0, x1, y1, color);
        pxl8_cpu_draw_line_2d(cpu, x1, y1, x2, y2, color);
        pxl8_cpu_draw_line_2d(cpu, x2, y2, x0, y0, color);
    }
}

u8* pxl8_cpu_get_framebuffer(pxl8_cpu_backend* cpu) {
    if (!cpu || cpu->target_stack_depth == 0) return NULL;
    return cpu->target_stack[0]->framebuffer;
}

u32 pxl8_cpu_get_height(const pxl8_cpu_backend* cpu) {
    if (!cpu || cpu->target_stack_depth == 0) return 0;
    return cpu->target_stack[0]->height;
}

u32 pxl8_cpu_get_width(const pxl8_cpu_backend* cpu) {
    if (!cpu || cpu->target_stack_depth == 0) return 0;
    return cpu->target_stack[0]->width;
}

pxl8_cpu_render_target* pxl8_cpu_create_render_target(const pxl8_cpu_render_target_desc* desc) {
    if (!desc) return NULL;

    pxl8_cpu_render_target* target = calloc(1, sizeof(pxl8_cpu_render_target));
    if (!target) return NULL;

    u32 size = desc->width * desc->height;
    target->width = desc->width;
    target->height = desc->height;
    target->framebuffer = calloc(size, sizeof(u8));
    if (!target->framebuffer) {
        free(target);
        return NULL;
    }

    if (desc->with_depth) {
        target->zbuffer = calloc(size, sizeof(u16));
        if (!target->zbuffer) {
            free(target->framebuffer);
            free(target);
            return NULL;
        }
    }

    if (desc->with_lighting) {
        target->light_accum = calloc(size, sizeof(u32));
        if (!target->light_accum) {
            free(target->zbuffer);
            free(target->framebuffer);
            free(target);
            return NULL;
        }
    }

    return target;
}

void pxl8_cpu_destroy_render_target(pxl8_cpu_render_target* target) {
    if (!target) return;
    free(target->light_accum);
    free(target->zbuffer);
    free(target->framebuffer);
    free(target);
}

pxl8_cpu_render_target* pxl8_cpu_get_target(pxl8_cpu_backend* cpu) {
    return cpu ? cpu->current_target : NULL;
}

void pxl8_cpu_set_target(pxl8_cpu_backend* cpu, pxl8_cpu_render_target* target) {
    if (!cpu || !target) return;
    cpu->current_target = target;
}

void pxl8_cpu_blit(pxl8_cpu_backend* cpu, pxl8_cpu_render_target* src, i32 x, i32 y, u8 transparent_idx) {
    if (!cpu || !cpu->current_target || !src || !src->framebuffer) return;
    pxl8_cpu_render_target* dst = cpu->current_target;

    i32 src_x0 = 0, src_y0 = 0;
    i32 dst_x0 = x, dst_y0 = y;
    i32 copy_w = (i32)src->width;
    i32 copy_h = (i32)src->height;

    if (dst_x0 < 0) { src_x0 = -dst_x0; copy_w += dst_x0; dst_x0 = 0; }
    if (dst_y0 < 0) { src_y0 = -dst_y0; copy_h += dst_y0; dst_y0 = 0; }
    if (dst_x0 + copy_w > (i32)dst->width) copy_w = (i32)dst->width - dst_x0;
    if (dst_y0 + copy_h > (i32)dst->height) copy_h = (i32)dst->height - dst_y0;

    if (copy_w <= 0 || copy_h <= 0) return;

    for (i32 row = 0; row < copy_h; row++) {
        u8* src_row = src->framebuffer + (src_y0 + row) * src->width + src_x0;
        u8* dst_row = dst->framebuffer + (dst_y0 + row) * dst->width + dst_x0;
        u32* light_row = dst->light_accum ? dst->light_accum + (dst_y0 + row) * dst->width + dst_x0 : NULL;
        for (i32 col = 0; col < copy_w; col++) {
            u8 pixel = src_row[col];
            if (pixel != transparent_idx) {
                dst_row[col] = pixel;
                if (light_row) light_row[col] = 0;
            }
        }
    }
}

bool pxl8_cpu_push_target(pxl8_cpu_backend* cpu) {
    if (!cpu || cpu->target_stack_depth >= PXL8_MAX_TARGET_STACK) return false;

    pxl8_cpu_render_target* base = cpu->target_stack[0];
    pxl8_cpu_render_target_desc desc = {
        .width = base->width,
        .height = base->height,
        .with_depth = false,
        .with_lighting = false,
    };
    pxl8_cpu_render_target* target = pxl8_cpu_create_render_target(&desc);
    if (!target) return false;

    cpu->target_stack[cpu->target_stack_depth++] = target;
    cpu->current_target = target;
    return true;
}

void pxl8_cpu_pop_target(pxl8_cpu_backend* cpu) {
    if (!cpu || cpu->target_stack_depth <= 1) return;

    pxl8_cpu_render_target* popped = cpu->target_stack[--cpu->target_stack_depth];
    cpu->current_target = cpu->target_stack[cpu->target_stack_depth - 1];

    pxl8_cpu_blit(cpu, popped, 0, 0, 0);
    pxl8_cpu_destroy_render_target(popped);
}

u32 pxl8_cpu_get_target_depth(const pxl8_cpu_backend* cpu) {
    return cpu ? cpu->target_stack_depth : 0;
}

u8* pxl8_cpu_render_target_get_framebuffer(pxl8_cpu_render_target* target) {
    return target ? target->framebuffer : NULL;
}

u32 pxl8_cpu_render_target_get_height(const pxl8_cpu_render_target* target) {
    return target ? target->height : 0;
}

u32 pxl8_cpu_render_target_get_width(const pxl8_cpu_render_target* target) {
    return target ? target->width : 0;
}

u32* pxl8_cpu_render_target_get_light_accum(pxl8_cpu_render_target* target) {
    return target ? target->light_accum : NULL;
}

u32* pxl8_cpu_get_output(pxl8_cpu_backend* cpu) {
    return cpu ? cpu->output : NULL;
}

void pxl8_cpu_render_glows(
    pxl8_cpu_backend* cpu,
    const pxl8_glow_source* glows,
    u32 glow_count
) {
    if (!cpu || cpu->target_stack_depth == 0) return;

    pxl8_cpu_render_target* target = cpu->target_stack[cpu->target_stack_depth - 1];
    if (!target || !target->framebuffer) return;

    u32 width = target->width;
    u32 height = target->height;
    u8* pixels = target->framebuffer;
    u16* zbuffer = target->zbuffer;
    u32* light_accum = target->light_accum;

    for (u32 gi = 0; gi < glow_count; gi++) {
        const pxl8_glow_source* glow = &glows[gi];
        i32 cx = glow->x;
        i32 cy = glow->y;
        i32 radius = glow->radius;

        if (radius <= 0 || glow->intensity == 0) continue;

        i32 x0 = cx - radius;
        i32 y0 = cy - radius;
        i32 x1 = cx + radius;
        i32 y1 = cy + radius;

        if (x0 < 0) x0 = 0;
        if (y0 < 0) y0 = 0;
        if (x1 >= (i32)width) x1 = (i32)width - 1;
        if (y1 >= (i32)height) y1 = (i32)height - 1;

        if (x0 >= x1 || y0 >= y1) continue;

        u16 glow_depth = glow->depth;
        u8 base_color = glow->color;
        f32 base_intensity = glow->intensity / 255.0f;
        f32 radius_f = (f32)radius;
        f32 radius_sq = radius_f * radius_f;
        f32 inv_radius_sq = 1.0f / radius_sq;
        f32 inv_radius = 1.0f / radius_f;

        for (i32 y = y0; y <= y1; y++) {
            u32 row = (u32)y * width;
            f32 dy = (f32)(y - cy);

            for (i32 x = x0; x <= x1; x++) {
                u32 idx = row + (u32)x;

                if (zbuffer && glow_depth > zbuffer[idx]) continue;

                f32 dx = (f32)(x - cx);
                f32 intensity = 0.0f;

                switch (glow->shape) {
                    case PXL8_GLOW_CIRCLE: {
                        f32 dist_sq = dx * dx + dy * dy;
                        if (dist_sq >= radius_sq) continue;
                        f32 falloff = 1.0f - dist_sq * inv_radius_sq;
                        intensity = base_intensity * falloff * falloff;
                        break;
                    }
                    case PXL8_GLOW_DIAMOND: {
                        f32 abs_dx = dx < 0 ? -dx : dx;
                        f32 abs_dy = dy < 0 ? -dy : dy;
                        f32 manhattan = abs_dx + abs_dy;
                        if (manhattan >= radius_f) continue;
                        f32 falloff = 1.0f - manhattan * inv_radius;
                        intensity = base_intensity * falloff * falloff;
                        break;
                    }
                    case PXL8_GLOW_SHAFT: {
                        f32 abs_dx = dx < 0 ? -dx : dx;
                        f32 abs_dy = dy < 0 ? -dy : dy;
                        f32 height_f = glow->height > 0 ? (f32)glow->height : radius_f;
                        if (abs_dx >= radius_f || abs_dy >= height_f) continue;
                        f32 falloff_x = 1.0f - abs_dx * inv_radius;
                        f32 falloff_y = 1.0f - abs_dy / height_f;
                        intensity = base_intensity * falloff_x * falloff_x * falloff_y;
                        break;
                    }
                }

                if (intensity < 0.02f) continue;

                u8 int_val = intensity < 1.0f ? (u8)(intensity * 255.0f) : 255;
                u8 light_level = pxl8_ordered_dither(int_val, (u32)x, (u32)y);

                if (light_level < 8) continue;

                u8 shaded = pxl8_colormap_lookup(cpu->colormap, base_color, PXL8_LIGHT_WHITE, light_level);

                if (cpu->palette_cube) {
                    u8 sr, sg, sb, dr, dg, db;
                    pxl8_palette_cube_get_rgb(cpu->palette_cube, shaded, &sr, &sg, &sb);
                    pxl8_palette_cube_get_rgb(cpu->palette_cube, pixels[idx], &dr, &dg, &db);
                    u32 r = (u32)sr + (u32)dr;
                    u32 g = (u32)sg + (u32)dg;
                    u32 b = (u32)sb + (u32)db;
                    if (r > 255) r = 255;
                    if (g > 255) g = 255;
                    if (b > 255) b = 255;
                    pixels[idx] = pxl8_palette_cube_lookup(cpu->palette_cube, (u8)r, (u8)g, (u8)b);
                } else {
                    pixels[idx] = shaded;
                }

                if (light_accum) {
                    light_accum[idx] = 0;
                }
            }
        }
    }
}

void pxl8_cpu_resolve(pxl8_cpu_backend* cpu) {
    if (!cpu || !cpu->palette || cpu->target_stack_depth == 0) return;

    pxl8_cpu_render_target* target = cpu->target_stack[0];
    if (!target || !target->framebuffer) return;

    u32 pixel_count = target->width * target->height;
    const u8* pixels = target->framebuffer;
    const u32* light_accum = target->light_accum;
    const u32* palette = cpu->palette;
    u32* output = cpu->output;

    for (u32 i = 0; i < pixel_count; i++) {
        u32 base = palette[pixels[i]];

        if (light_accum && light_accum[i] != 0) {
            u32 light_color = light_accum[i];
            u32 tint_alpha = (light_color >> 24) & 0xFF;

            if (tint_alpha > 0) {
                u32 lr = light_color & 0xFF;
                u32 lg = (light_color >> 8) & 0xFF;
                u32 lb = (light_color >> 16) & 0xFF;

                u32 br = base & 0xFF;
                u32 bg = (base >> 8) & 0xFF;
                u32 bb = (base >> 16) & 0xFF;
                u32 ba = (base >> 24) & 0xFF;

                u32 t = (tint_alpha * tint_alpha) / 255;
                u32 inv_t = 255 - t;

                u32 lr_norm = (lr * 255) / 240;
                u32 lg_norm = (lg * 255) / 240;
                u32 lb_norm = (lb * 255) / 220;

                u32 or = ((br * inv_t + (br * lr_norm / 255) * t) / 255);
                u32 og = ((bg * inv_t + (bg * lg_norm / 255) * t) / 255);
                u32 ob = ((bb * inv_t + (bb * lb_norm / 255) * t) / 255);

                if (or > 255) or = 255;
                if (og > 255) og = 255;
                if (ob > 255) ob = 255;

                output[i] = or | (og << 8) | (ob << 16) | (ba << 24);
                continue;
            }
        }

        output[i] = base;
    }

    for (u32 t = 1; t < cpu->target_stack_depth; t++) {
        pxl8_cpu_render_target* overlay = cpu->target_stack[t];
        if (!overlay || !overlay->framebuffer) continue;

        const u8* overlay_pixels = overlay->framebuffer;
        for (u32 i = 0; i < pixel_count; i++) {
            u8 idx = overlay_pixels[i];
            if (idx != 0) {
                output[i] = palette[idx];
            }
        }
    }
}
