local sin, cos, abs, flr, ceil = sin, cos, abs, flr, ceil

-- simple deterministic RNG for shake/glitch
local _rng = 0x12345678
local function irnd(n)
  _rng = (_rng * 1103515245 + 12345) & 0x7fffffff
  return _rng % n
end

local CFG = {
  w=240, h=135,
  horizon=64,
  focal=80,
  near=0.35,

  -- horizon background styling
  hz_offset=4,     -- pixels below the projection horizon to draw the glow line
  hz_peak=13,      -- brightest color index at horizon center (0-63)
  hz_spread=40,    -- gradient height in scanlines (extends upward only)

  cam_y=6,
  pitch=-0.01,

  scroll=12.5,
  z_start=5.0,
  dz=8.2,

  cols=16,
  rows=6,
  dx=6.8,
  cam_sway=0.0,
  grid_warp=0.0,

  -- far vertical lod
  far_v_from_row=14,
  far_v_every=2,

  -- grid beam brightness for userdata-aa path
  grid_core_near=40,
  grid_core_far=0,
  grid_halo_near=6,
  grid_halo_far=0,
  grid_halo_rows=10,

  -- rows from camera that get full AA lines; beyond this fades to nothing
  grid_fade_rows=6,

  -- terrain peak controls
  peak_master=0.0,

  peak1_amp=0.35,
  peak1_freq_x=0.45,
  peak1_freq_z=0.11,
  peak1_speed=1.15,
  peak1_dir_x=0.0,
  peak1_dir_z=1.0,

  peak2_amp=0.02,
  peak2_freq_x=0.00,
  peak2_freq_z=0.19,
  peak2_speed=0.72,
  peak2_dir_x=0.0,
  peak2_dir_z=-1.0,

  peak3_amp=0.08,
  peak3_freq_x=0.16,
  peak3_freq_z=0.05,
  peak3_speed=0.55,
  peak3_dir_x=1.0,
  peak3_dir_z=0.35,

  -- morphing object
  obj_scale=2.45,
  obj_z=13.0,
  obj_y=4.0,
  obj_bob=0.35,
  obj_core=42,
  obj_halo=12,
  morph_speed=0.15,

  -- scratch buffer height for aa rasterizer
  aa_batch_rows=768,

  -- avoid drawing identical mesh edges in the same screen space
  mesh_dedupe=true,

  -- mesh processing batch size (0 = all at once)
  mesh_batch=512,
}

local W,H = CFG.w, CFG.h
local CX = W * 0.5
local HZ = CFG.horizon
local CP = cos(CFG.pitch)
local SP = sin(CFG.pitch)

local COLS1 = CFG.cols + 1
local ROWS1 = CFG.rows + 1

local aa_batch = userdata("f64", 3, CFG.aa_batch_rows)

-- pixel accumulation buffer: one big pset per frame instead of per-line
local PIX_MAX = 32768
local pix_buf = userdata("f64", 3, PIX_MAX)
local pix_n = 0

local base_x = {}
local grid_world_ud = nil
local grid_view_ud = nil
local view_matrix = userdata("f64", 4, 4)
local view_matrix_t = userdata("f64", 4, 4)
local use_matmul3d = false
local mesh_shapes = {}
local mesh_edges = nil
local mesh_vert_count = 0
local edge_seen = {}
local mesh_ud_shapes = {}
local mesh_local_ud = nil
local mesh_view_ud = nil
local mesh_matrix = nil
local mesh_matrix_t = nil
local mesh_combined = nil
local mesh_combined_t = nil

-- base palette colors (stored for fade)
local base_pal = {}

local function clamp(v,a,b)
  if v < a then return a end
  if v > b then return b end
  return v
end

local function mix(a,b,t)
  return a + (b-a) * t
end

local function smoothstep(t)
  return t*t*(3-2*t)
end

local function smoothstep2(t)
  -- stronger ease to spend more time near the endpoints
  t = smoothstep(t)
  return smoothstep(t)
end

local function idx(r,c)
  return (r-1) * COLS1 + c
end

local function mat4_mul(a, b, dst)
  for r=0,3 do
    for c=0,3 do
      local s = 0
      for k=0,3 do
        s = s + a:get(k, r) * b:get(c, k)
      end
      dst:set(c, r, s)
    end
  end
end

-- inline mesh data (42 verts, 120 edges, 4 shapes)
local MESH_EDGES = {
  {13,1},{14,2},{15,3},{16,2},{17,6},{18,1},
  {19,4},{20,1},{21,5},{22,6},{23,2},{24,11},
  {25,3},{26,7},{27,4},{28,8},{29,5},{30,9},
  {31,6},{32,10},{33,7},{34,8},{35,9},{36,10},
  {37,11},{38,7},{39,12},{40,8},{41,9},{42,10},
  {3,13},{1,14},{2,15},{6,16},{1,17},{4,18},
  {3,19},{5,20},{4,21},{5,22},{11,23},{6,24},
  {7,25},{2,26},{8,27},{3,28},{9,29},{4,30},
  {10,31},{5,32},{11,33},{7,34},{8,35},{9,36},
  {10,37},{12,38},{11,39},{12,40},{12,41},{12,42},
  {39,42},{39,37},{42,37},{42,41},{42,36},{41,36},
  {41,40},{41,35},{40,35},{40,38},{40,34},{38,34},
  {38,39},{38,33},{39,33},{24,37},{24,31},{37,31},
  {32,36},{32,29},{36,29},{30,35},{30,27},{35,27},
  {28,34},{28,25},{34,25},{26,33},{26,23},{33,23},
  {31,32},{31,22},{32,22},{29,30},{29,21},{30,21},
  {27,28},{27,19},{28,19},{25,26},{25,15},{26,15},
  {23,24},{23,16},{24,16},{17,22},{17,20},{22,20},
  {20,21},{20,18},{21,18},{18,19},{18,13},{19,13},
  {16,17},{16,14},{17,14},{13,15},{13,14},{15,14},
}

local MESH_SHAPE_1 = {
  {-0.3228,-0.6090,-2.9529},{0.6892,-0.4986,-0.4096},
  {-0.2587,-0.7919,-0.4121},{-0.8833,-0.1579,-0.4307},
  {-0.2515,0.7741,-0.4070},{3.2343,0.9567,-1.6357},
  {0.2515,-0.7741,0.4070},{-1.7209,-1.7184,0.8442},
  {-0.9232,0.4240,0.4934},{-0.0177,0.9820,0.5071},
  {0.8654,-0.0315,0.4086},{-0.0241,0.0016,0.9399},
  {-0.1478,-0.4550,-0.7741},{0.3811,-0.2925,-0.8119},
  {0.2392,-0.7362,-0.4784},{0.7742,-0.0001,-0.4784},
  {0.3387,0.1899,-1.0804},{-0.5183,0.0077,-0.7822},
  {-0.6263,-0.4550,-0.4784},{-0.1725,0.4085,-0.9299},
  {-2.4446,0.8082,-0.8463},{0.2392,0.7362,-0.4784},
  {3.0886,-1.7467,-0.1922},{0.8665,0.2805,-0.0001},
  {-0.6882,-2.9309,-0.7496},{0.5349,-0.7362,0.0000},
  {-1.1736,-0.6556,0.1312},{-0.5425,-0.7450,0.0032},
  {-0.5838,0.7842,-0.0113},{-1.1640,0.1629,-0.0177},
  {0.5349,0.7362,0.0000},{-0.8438,4.0200,-0.1654},
  {0.6263,-0.4550,0.4784},{-0.2392,-0.7362,0.4784},
  {-0.9473,-0.2021,0.5497},{-0.2492,0.7431,0.4822},
  {0.6264,0.4550,0.4786},{0.1478,-0.4550,0.7741},
  {2.1827,-0.3585,2.5543},{-0.3870,-0.2812,0.7741},
  {-3.2492,0.6101,2.0832},{0.1381,0.4561,0.7785},
}

local MESH_SHAPE_2 = {
  {0.0938,-0.0006,-1.9177},{1.4008,-1.0527,-0.8795},
  {-0.5450,-1.6773,-0.8818},{-1.0306,0.0115,-0.3965},
  {-0.5450,1.6773,-0.8818},{1.4268,1.0366,-0.8818},
  {0.5450,-1.6773,0.8818},{-1.4268,-1.0366,0.8818},
  {-1.4268,1.0366,0.8818},{0.5450,1.6773,0.8818},
  {1.0810,0.0227,0.4192},{0.0000,0.0000,1.9718},
  {-0.3203,-0.9859,-1.6773},{0.8350,-0.6115,-1.6770},
  {0.4216,-0.8950,-0.6024},{1.6773,0.0000,-1.0366},
  {0.4298,0.2899,-0.9370},{-1.0366,0.0000,-1.6773},
  {-1.3278,-0.8805,-1.0470},{-0.3088,0.9858,-1.6706},
  {-1.3570,0.9859,-1.0366},{0.5183,1.5952,-1.0367},
  {1.8753,-0.6093,0.0000},{1.8688,0.5888,0.0044},
  {0.0000,-1.9718,0.0000},{1.1590,-1.5952,0.0000},
  {-1.8628,-0.5642,-0.0044},{-1.1590,-1.5952,0.0000},
  {-1.0737,1.5771,-0.0000},{-1.8753,0.6093,0.0000},
  {1.1590,1.5952,0.0000},{-0.0500,1.0958,0.1170},
  {1.3245,-0.9587,1.0035},{-0.5183,-1.5952,1.0367},
  {-1.6773,0.0000,1.0366},{-0.5183,1.5952,1.0367},
  {1.3569,0.9857,1.0367},{0.3203,-0.9859,1.6773},
  {1.0366,0.0000,1.6773},{-0.8387,-0.6093,1.6773},
  {-0.8387,0.6093,1.6773},{0.3203,0.9859,1.6773},
}

local MESH_SHAPE_3 = {
  {2.1312,2.0692,-0.5805},{1.9459,0.6991,0.1592},
  {1.5294,0.5270,-1.6758},{0.4362,2.1674,-1.5277},
  {0.6717,2.3117,-0.3717},{2.4585,1.9981,1.1700},
  {-0.8539,-2.2534,0.4531},{-0.5283,-0.0719,-1.4903},
  {-0.8348,2.0921,-0.3752},{0.2864,1.2517,0.9823},
  {1.5712,0.6543,0.4114},{-0.2853,0.1100,-0.4280},
  {2.4241,2.3956,-1.9463},{1.9367,1.2849,-0.0235},
  {1.6195,0.6349,-0.7352},{2.3687,1.4181,0.7340},
  {2.2216,1.9284,0.5014},{1.5516,2.3580,-1.5149},
  {0.6506,0.9673,-1.3673},{1.6154,2.4577,-0.5161},
  {1.4425,4.2373,-1.3469},{1.6185,2.2798,0.7314},
  {1.9879,0.6194,0.5048},{3.2131,1.8430,1.8916},
  {0.4534,-0.6655,-0.8236},{1.2928,-0.2282,-0.4367},
  {-0.4225,1.4259,-1.3289},{0.5707,-0.1715,-2.7361},
  {-0.0980,2.3756,-0.2436},{-0.1907,2.3633,-0.9066},
  {1.5006,1.6920,1.7325},{0.5717,2.0913,0.7978},
  {0.9126,-0.2922,-0.5946},{-0.5925,-1.1038,-0.5947},
  {-1.5171,1.8353,-0.6295},{-0.1774,1.7368,0.1489},
  {1.2577,1.1075,0.5520},{-0.6423,-1.2903,0.0551},
  {0.6754,0.4168,-0.4328},{-0.6132,-0.2016,-0.6704},
  {-0.8217,1.3223,-0.3210},{0.1355,1.0920,-0.0443},
}

local MESH_SHAPE_4 = {
  {0.0000,0.0000,-0.2565},{0.6909,-1.3595,-1.4970},
  {-0.3590,-1.1229,-0.7638},{-1.2651,-0.0000,-0.8511},
  {-0.3590,1.1229,-0.7638},{0.6909,1.3595,-1.4970},
  {0.3590,-1.1229,0.7638},{-0.6909,-1.3595,1.4970},
  {-0.6909,1.3595,1.4970},{0.3590,1.1229,0.7638},
  {1.2651,-0.0000,0.8511},{0.0000,0.0000,0.2565},
  {-0.2025,-0.7518,-1.1035},{0.5242,-0.5514,-1.1704},
  {0.3284,-1.1199,-0.8764},{1.1486,0.0000,-0.9647},
  {0.5242,0.5514,-1.1704},{-0.6744,0.0000,-1.0930},
  {-0.6444,-1.3200,-1.6093},{-0.2025,0.7518,-1.1035},
  {-0.6444,1.3200,-1.6093},{0.3284,1.1199,-0.8764},
  {2.5977,-0.4755,0.0000},{2.5977,0.4755,0.0000},
  {0.0000,-0.1309,0.0000},{0.8763,-1.1181,-0.0000},
  {-2.5977,-0.4755,-0.0000},{-0.8763,-1.1181,0.0000},
  {-0.8763,1.1181,0.0000},{-2.5977,0.4755,0.0000},
  {0.8763,1.1181,-0.0000},{0.0000,0.1309,-0.0000},
  {0.6444,-1.3200,1.6093},{-0.3284,-1.1199,0.8764},
  {-1.1486,0.0000,0.9647},{-0.3284,1.1199,0.8764},
  {0.6444,1.3200,1.6093},{0.2025,-0.7518,1.1035},
  {0.6744,0.0000,1.0930},{-0.5242,-0.5514,1.1704},
  {-0.5242,0.5514,1.1704},{0.2025,0.7518,1.1035},
}

local MESH_DATA = {
  vert_count = 42,
  edge_count = 120,
  num_shapes = 4,
  edges = MESH_EDGES,
  shapes = {MESH_SHAPE_1, MESH_SHAPE_2, MESH_SHAPE_3, MESH_SHAPE_4},
}

-- load mesh data from a table
local function load_meshes(data)
  mesh_shapes = {}
  mesh_edges = nil
  mesh_vert_count = 0

  if not data or type(data) ~= "table" then return end

  mesh_vert_count = data.vert_count or 0
  mesh_edges = data.edges
  local num_shapes = data.num_shapes or 0

  if mesh_vert_count == 0 or not mesh_edges or num_shapes < 2 then
    mesh_edges = nil
    mesh_vert_count = 0
    return
  end

  for si=1,num_shapes do
    mesh_shapes[si] = { verts = data.shapes[si] }
  end

  mesh_ud_shapes = {}
  mesh_local_ud = userdata("f64", 4, mesh_vert_count)
  mesh_view_ud = userdata("f64", 4, mesh_vert_count)
  mesh_matrix = userdata("f64", 4, 4)
  mesh_matrix_t = userdata("f64", 4, 4)
  mesh_combined = userdata("f64", 4, 4)
  mesh_combined_t = userdata("f64", 4, 4)

  for si=1,num_shapes do
    local ud = userdata("f64", 4, mesh_vert_count)
    local verts = mesh_shapes[si].verts
    for vi=1,mesh_vert_count do
      local v = verts[vi]
      ud:set(0, vi-1, v[1])
      ud:set(1, vi-1, v[2])
      ud:set(2, vi-1, v[3])
      ud:set(3, vi-1, 1)
    end
    mesh_ud_shapes[si] = ud
  end
end

local function set_rgb_ramp()
  for i=0,63 do
    local t = i / 63
    local r,g,b

    if t < 0.40 then
      local u = t / 0.40
      r = flr(  0 +  15*u + 0.5)
      g = flr(  0 + 100*u + 0.5)
      b = flr(  0 + 145*u + 0.5)
    elseif t < 0.80 then
      local u = (t - 0.40) / 0.40
      r = flr( 15 +  45*u + 0.5)
      g = flr(100 + 125*u + 0.5)
      b = flr(145 + 105*u + 0.5)
    else
      local u = (t - 0.80) / 0.20
      r = flr( 60 + 195*u + 0.5)
      g = flr(225 +  30*u + 0.5)
      b = flr(250 +   5*u + 0.5)
    end

    local c = (r<<16) | (g<<8) | b
    pal(i, c, 2)
    base_pal[i] = c
  end
end

local function build_add_table()
  for draw=0,63 do
    local a = 0x8000 + draw * 64
    for target=0,63 do
      local out = draw + target
      if out > 63 then out = 63 end
      poke(a + target, out)
    end
  end
end

local function wave_shift(wx, wz, t, speed, dx, dz)
  return wx + t*speed*dx, wz + t*speed*dz
end

local function terrain(wx, wz, t)
  local x1,z1 = wave_shift(wx, wz, t, CFG.peak1_speed, CFG.peak1_dir_x, CFG.peak1_dir_z)
  local x2,z2 = wave_shift(wx, wz, t, CFG.peak2_speed, CFG.peak2_dir_x, CFG.peak2_dir_z)
  local x3,z3 = wave_shift(wx, wz, t, CFG.peak3_speed, CFG.peak3_dir_x, CFG.peak3_dir_z)

  local h =
    sin(x1*CFG.peak1_freq_x + z1*CFG.peak1_freq_z) * CFG.peak1_amp +
    cos(x2*CFG.peak2_freq_x + z2*CFG.peak2_freq_z) * CFG.peak2_amp +
    sin(x3*CFG.peak3_freq_x) * cos(z3*CFG.peak3_freq_z) * CFG.peak3_amp

  return h * CFG.peak_master
end

local function clip_near(x0, y0, z0, x1, y1, z1)
  if z0 <= CFG.near and z1 <= CFG.near then
    return nil
  end

  if z0 <= CFG.near then
    local t = (CFG.near - z0) / (z1 - z0)
    x0 = x0 + (x1 - x0) * t
    y0 = y0 + (y1 - y0) * t
    z0 = CFG.near
  elseif z1 <= CFG.near then
    local t = (CFG.near - z0) / (z1 - z0)
    x1 = x0 + (x1 - x0) * t
    y1 = y0 + (y1 - y0) * t
    z1 = CFG.near
  end

  return x0, y0, z0, x1, y1, z1
end

local function project(vx, vy, vz)
  local s = CFG.focal / vz
  return CX + vx * s, HZ - vy * s
end

local function outcode(x, y)
  local c = 0
  if x < 0 then
    c = c | 1
  elseif x > W-1 then
    c = c | 2
  end

  if y < 0 then
    c = c | 8
  elseif y > H-1 then
    c = c | 4
  end

  return c
end

local function clip2d(x0, y0, x1, y1)
  local c0 = outcode(x0, y0)
  local c1 = outcode(x1, y1)

  while true do
    if (c0 | c1) == 0 then
      return x0, y0, x1, y1
    end

    if (c0 & c1) ~= 0 then
      return nil
    end

    local co = c0 ~= 0 and c0 or c1
    local x, y

    if (co & 8) ~= 0 then
      if y1 == y0 then return nil end
      x = x0 + (x1 - x0) * ((0 - y0) / (y1 - y0))
      y = 0
    elseif (co & 4) ~= 0 then
      if y1 == y0 then return nil end
      x = x0 + (x1 - x0) * (((H-1) - y0) / (y1 - y0))
      y = H-1
    elseif (co & 2) ~= 0 then
      if x1 == x0 then return nil end
      y = y0 + (y1 - y0) * (((W-1) - x0) / (x1 - x0))
      x = W-1
    else
      if x1 == x0 then return nil end
      y = y0 + (y1 - y0) * ((0 - x0) / (x1 - x0))
      x = 0
    end

    if co == c0 then
      x0, y0 = x, y
      c0 = outcode(x0, y0)
    else
      x1, y1 = x, y
      c1 = outcode(x1, y1)
    end
  end
end

local function flush_lines()
  if pix_n > 0 then
    pset(pix_buf, 0, pix_n)
    pix_n = 0
  end
end

local function plot_point(x, y, col)
  x = flr(x + 0.5)
  y = flr(y + 0.5)
  if x >= 0 and x < W and y >= 0 and y < H then
    if pix_n >= PIX_MAX then flush_lines() end
    pix_buf:set(0, pix_n, x, y, col)
    pix_n = pix_n + 1
  end
end

local function aaline(x1, y1, x2, y2, base)
  base = clamp(flr(base + 0.5), 1, 63)

  x1, y1, x2, y2 = clip2d(x1, y1, x2, y2)
  if not x1 then return end

  local dx = x2 - x1
  local dy = y2 - y1

  if abs(dx) < 0.0001 and abs(dy) < 0.0001 then
    plot_point(x1, y1, base)
    return
  end

  if abs(dx) > abs(dy) then
    if x2 < x1 then
      x1,y1,x2,y2 = x2,y2,x1,y1
      dx = x2 - x1
      dy = y2 - y1
    end

    if abs(dx) < 0.0001 then
      plot_point(x1, y1, base)
      return
    end

    local grad = dy / dx
    local ix1 = ceil(x1)
    local ix2 = ceil(x2)
    local yy  = y1 + (ix1 - x1) * grad
    local len = ix2 - ix1

    if len <= 0 then
      plot_point(x1, y1, base)
      return
    end

    aa_batch:set(0, 0,   ix1, yy, yy * base)
    aa_batch:set(0, len, ix2, y2, y2 * base)
    aa_batch:lerp(0, len, 3, 3, 1)
    aa_batch:mod(base, true, 0, 2, 1, 0, 3, len)

    aa_batch:copy(aa_batch, true, 0, len*3, len*3, 0, 0, 1)
    aa_batch:add(1, true, 0, len*3+1, 1, 0, 3, len)

    aa_batch:copy(base, true, 0, 2, 1, 0, 3, len)
    aa_batch:sub(aa_batch, true, len*3+2, 2, 1, 3, 3, len)

    if pix_n + len*2 > PIX_MAX then flush_lines() end
    pix_buf:copy(aa_batch, true, 0, pix_n*3, len*2*3, 0, 0, 1)
    pix_n = pix_n + len*2
  else
    if y2 < y1 then
      x1,y1,x2,y2 = x2,y2,x1,y1
      dx = x2 - x1
      dy = y2 - y1
    end

    if abs(dy) < 0.0001 then
      plot_point(x1, y1, base)
      return
    end

    local iy1 = ceil(y1)
    local iy2 = ceil(y2)
    local grad = dx / dy
    local xx = x1 + (iy1 - y1) * grad
    local xx2 = x2 + (iy2 - y2) * grad
    local len = iy2 - iy1

    if len <= 0 then
      plot_point(x1, y1, base)
      return
    end

    aa_batch:set(0, 0,   xx,  iy1, xx  * base)
    aa_batch:set(0, len, xx2, iy2, xx2 * base)
    aa_batch:lerp(0, len, 3, 3, 1)
    aa_batch:mod(base, true, 0, 2, 1, 0, 3, len)

    aa_batch:copy(aa_batch, true, 0, len*3, len*3, 0, 0, 1)
    aa_batch:add(1, true, 0, len*3, 1, 0, 3, len)

    aa_batch:copy(base, true, 0, 2, 1, 0, 3, len)
    aa_batch:sub(aa_batch, true, len*3+2, 2, 1, 3, 3, len)

    if pix_n + len*2 > PIX_MAX then flush_lines() end
    pix_buf:copy(aa_batch, true, 0, pix_n*3, len*2*3, 0, 0, 1)
    pix_n = pix_n + len*2
  end
end

local function aabeam(x0, y0, x1, y1, core, halo)
  core = clamp(core, 0, 63)
  halo = clamp(halo, 0, 63)

  if halo > 0 then
    if abs(x1 - x0) >= abs(y1 - y0) then
      aaline(x0, y0-1, x1, y1-1, halo)
      aaline(x0, y0+1, x1, y1+1, halo)
    else
      aaline(x0-1, y0, x1-1, y1, halo)
      aaline(x0+1, y0, x1+1, y1, halo)
    end
  end

  if core > 0 then
    aaline(x0, y0, x1, y1, core)
  end
end

local function clip_project(vx0, vy0, vz0, vx1, vy1, vz1)
  vx0, vy0, vz0, vx1, vy1, vz1 = clip_near(vx0, vy0, vz0, vx1, vy1, vz1)
  if not vx0 then return nil end

  local sx0, sy0 = project(vx0, vy0, vz0)
  local sx1, sy1 = project(vx1, vy1, vz1)

  if (sx0 < -32 and sx1 < -32) or (sx0 > W+31 and sx1 > W+31) or
     (sy0 < -32 and sy1 < -32) or (sy0 > H+31 and sy1 > H+31) then
    return nil
  end

  return sx0, sy0, sx1, sy1
end

local function draw_seg(vx0, vy0, vz0, vx1, vy1, vz1, core, halo)
  local sx0, sy0, sx1, sy1 = clip_project(vx0, vy0, vz0, vx1, vy1, vz1)
  if not sx0 then return end
  aabeam(sx0, sy0, sx1, sy1, core, halo)
end

local function draw_seg_dedupe(vx0, vy0, vz0, vx1, vy1, vz1, core, halo)
  local sx0, sy0, sx1, sy1 = clip_project(vx0, vy0, vz0, vx1, vy1, vz1)
  if not sx0 then return end

  local ix0 = flr(sx0 + 0.5)
  local iy0 = flr(sy0 + 0.5)
  local ix1 = flr(sx1 + 0.5)
  local iy1 = flr(sy1 + 0.5)

  if ix1 < ix0 or (ix1 == ix0 and iy1 < iy0) then
    ix0, ix1 = ix1, ix0
    iy0, iy1 = iy1, iy0
  end

  local key = ix0..","..iy0..":"..ix1..","..iy1
  if edge_seen[key] then return end
  edge_seen[key] = true


  aabeam(sx0, sy0, sx1, sy1, core, halo)
end

local function draw_bg(hz_bright)
  cls(0)

  local hy = HZ + CFG.hz_offset
  rectfill(0, 0, W-1, hy-1, 1)
  rectfill(0, hy, W-1, H-1, 0)

  -- gradient glow: brightest at hy, fading upward
  local sp = CFG.hz_spread
  if sp < 1 then sp = 1 end
  local peak = CFG.hz_peak * hz_bright
  if peak < 1 then return end
  line(0, hy, W-1, hy, flr(peak + 0.5))
  for d = 1, sp do
    local f = 1 - d / (sp + 1)
    local c = flr(f * f * peak + 0.5)
    if c >= 1 then
      line(0, hy - d, W-1, hy - d, c)
    end
  end
end

local grid_frac = 0

local function build_grid(t, camx, camz)
  local zrow0 = flr(camz / CFG.dz)
  grid_frac = (camz / CFG.dz) - zrow0

  for r=1,ROWS1 do
    local wz = CFG.z_start + (zrow0 + r - 1) * CFG.dz
    local warp = sin(wz*0.04 + t*0.90) * CFG.grid_warp
    -- fade displacement to zero at the far row (horizon)
    local dfade = 1 - (r - 1) / (ROWS1 - 1)

    for c=1,COLS1 do
      local wx = base_x[c] + warp
      local wy = terrain(wx, wz, t) * dfade
      local i = (r-1) * COLS1 + (c-1)
      grid_world_ud:set(0, i, wx)
      grid_world_ud:set(1, i, wy)
      grid_world_ud:set(2, i, wz)
      grid_world_ud:set(3, i, 1)
    end
  end

  if use_matmul3d then
    grid_world_ud:matmul3d(view_matrix_t, grid_view_ud, 1)
  else
    matmul_fallback(grid_world_ud, grid_view_ud, COLS1 * ROWS1, view_matrix, 0)
  end
end

local function grid_seg(i0, i1, core, halo)
  local vx0 = grid_view_ud:get(0, i0)
  local vy0 = grid_view_ud:get(1, i0)
  local vz0 = grid_view_ud:get(2, i0)
  local vx1 = grid_view_ud:get(0, i1)
  local vy1 = grid_view_ud:get(1, i1)
  local vz1 = grid_view_ud:get(2, i1)

  local near = CFG.near
  if vz0 <= near and vz1 <= near then return end

  if vz0 <= near then
    local t = (near - vz0) / (vz1 - vz0)
    vx0 = vx0 + (vx1 - vx0) * t
    vy0 = vy0 + (vy1 - vy0) * t
    vz0 = near
  elseif vz1 <= near then
    local t = (near - vz0) / (vz1 - vz0)
    vx1 = vx0 + (vx1 - vx0) * t
    vy1 = vy0 + (vy1 - vy0) * t
    vz1 = near
  end

  local focal = CFG.focal
  local s0 = focal / vz0
  local s1 = focal / vz1
  local sx0 = CX + vx0 * s0
  local sy0 = HZ - vy0 * s0
  local sx1 = CX + vx1 * s1
  local sy1 = HZ - vy1 * s1

  aabeam(sx0, sy0, sx1, sy1, core, halo)
end

local function draw_grid(grid_bright)
  local fade = CFG.grid_fade_rows or ROWS1

  local function row_bright(r)
    local d = (r - 1 - grid_frac) / fade
    if d < 0 then d = 0 end
    if d >= 1 then return 0, 0 end
    local fm = 1 - d
    local core = flr((CFG.grid_core_far + (CFG.grid_core_near - CFG.grid_core_far) * fm) * fm * grid_bright + 0.5)
    local halo = flr((CFG.grid_halo_far + (CFG.grid_halo_near - CFG.grid_halo_far) * fm) * fm * grid_bright + 0.5)
    if r > CFG.grid_halo_rows then halo = 0 end
    return core, halo
  end

  -- horizontal lines
  for r=1,ROWS1 do
    local core, halo = row_bright(r)
    if core <= 0 and halo <= 0 then break end

    for c=1,CFG.cols do
      local i0 = (r-1) * COLS1 + (c-1)
      grid_seg(i0, i0 + 1, core, halo)
    end
  end

  -- vertical lines
  for r=1,CFG.rows do
    local core, halo = row_bright(r)
    if core <= 0 and halo <= 0 then break end

    local step = 1
    if r >= CFG.far_v_from_row then step = CFG.far_v_every end

    for c=1,COLS1,step do
      local i0 = (r-1) * COLS1 + (c-1)
      grid_seg(i0, i0 + COLS1, core, halo)
    end
  end
end

-- obj_morph is a float: integer part = source shape index (0-based, wraps),
-- fractional part = blend amount toward the next shape (smoothstepped).
local function morph_state(obj_morph)
  local num = #mesh_shapes
  if num < 2 then return 0, 1, 0 end
  local i0 = flr(obj_morph)
  local s0 = i0 % num
  local s1 = (i0 + 1) % num
  local u  = smoothstep2(obj_morph - i0)
  return s0, s1, u
end

local function build_matrix(m, yaw, pitch, roll, tx, ty, tz, s)
  local cy, sy = cos(yaw), sin(yaw)
  local cx, sx = cos(pitch), sin(pitch)
  local cz, sz = cos(roll), sin(roll)

  local sc = s or 1

  local m00 = (cz*cy + sz*sx*sy) * sc
  local m01 = (-sz*cx) * sc
  local m02 = (-cz*sy + sz*sx*cy) * sc
  local m10 = (sz*cy - cz*sx*sy) * sc
  local m11 = (cz*cx) * sc
  local m12 = (-sz*sy - cz*sx*cy) * sc
  local m20 = (cx*sy) * sc
  local m21 = (sx) * sc
  local m22 = (cx*cy) * sc

  m:set(0,0, m00) m:set(1,0, m01) m:set(2,0, m02) m:set(3,0, tx)
  m:set(0,1, m10) m:set(1,1, m11) m:set(2,1, m12) m:set(3,1, ty)
  m:set(0,2, m20) m:set(1,2, m21) m:set(2,2, m22) m:set(3,2, tz)
  m:set(0,3, 0)   m:set(1,3, 0)   m:set(2,3, 0)   m:set(3,3, 1)
end

local function matmul_fallback(src, dst, count, m, start)
  local base = start or 0
  for i=0,count-1 do
    local row = base + i
    local x = src:get(0,row)
    local y = src:get(1,row)
    local z = src:get(2,row)
    local w = src:get(3,row)
    local nx = m:get(0,0)*x + m:get(1,0)*y + m:get(2,0)*z + m:get(3,0)*w
    local ny = m:get(0,1)*x + m:get(1,1)*y + m:get(2,1)*z + m:get(3,1)*w
    local nz = m:get(0,2)*x + m:get(1,2)*y + m:get(2,2)*z + m:get(3,2)*w
    dst:set(0,row, nx)
    dst:set(1,row, ny)
    dst:set(2,row, nz)
    dst:set(3,row, 1)
  end
end

local function build_object_mesh(t, camx, camz, obj_x, obj_y, obj_z, obj_scale, obj_morph)
  local s0, s1, u = morph_state(obj_morph)
  local m0 = mesh_shapes[s0 + 1]
  local m1 = mesh_shapes[s1 + 1]
  local ud0 = mesh_ud_shapes[s0 + 1]
  local ud1 = mesh_ud_shapes[s1 + 1]
  if not m0 or not m1 or not ud0 or not ud1 or not mesh_view_ud then return s0, s1, u end

  local s  = obj_scale
  local cx = camx + obj_x
  local cy = obj_y + sin(t * 1.55) * CFG.obj_bob
  local cz = camz + obj_z

  local ay = t * 0.37
  local ax = t * 0.29
  local az = t * 0.21

  local batch = CFG.mesh_batch or 0
  if batch <= 0 then batch = mesh_vert_count end

  local i = 1
  while i <= mesh_vert_count do
    local last = i + batch - 1
    if last > mesh_vert_count then last = mesh_vert_count end
    for vi=i,last do
      local v0 = m0.verts[vi]
      local v1 = m1.verts[vi]
      mesh_local_ud:set(0, vi-1, mix(v0[1], v1[1], u))
      mesh_local_ud:set(1, vi-1, mix(v0[2], v1[2], u))
      mesh_local_ud:set(2, vi-1, mix(v0[3], v1[3], u))
      mesh_local_ud:set(3, vi-1, 1)
    end
    i = last + 1
  end

  build_matrix(mesh_matrix, ay, ax, az, cx, cy, cz, s)
  mat4_mul(view_matrix, mesh_matrix, mesh_combined)
  if use_matmul3d then
    mesh_combined:transpose(mesh_combined_t)
    mesh_local_ud:matmul3d(mesh_combined_t, mesh_view_ud, 1)
  else
    matmul_fallback(mesh_local_ud, mesh_view_ud, mesh_vert_count, mesh_combined, 0)
  end

  return s0, s1, u
end

local function draw_object_mesh(obj_core, obj_halo)
  if not mesh_edges then return end
  for i=1,#mesh_edges do
    local e = mesh_edges[i]
    local a = e[1]
    local b = e[2]
    if a and b then
      local ax = mesh_view_ud:get(0, a-1)
      local ay = mesh_view_ud:get(1, a-1)
      local az = mesh_view_ud:get(2, a-1)
      local bx = mesh_view_ud:get(0, b-1)
      local by = mesh_view_ud:get(1, b-1)
      local bz = mesh_view_ud:get(2, b-1)
      if CFG.mesh_dedupe then
        draw_seg_dedupe(
          ax, ay, az,
          bx, by, bz,
          obj_core, obj_halo
        )
      else
        draw_seg(
          ax, ay, az,
          bx, by, bz,
          obj_core, obj_halo
        )
      end
    end
  end
end

------------------------------------------------------------
-- rocket parameters
--
-- params[0]  = rocket_time (global)
-- params[1]  = part_id     (global)
-- params[2]  = t           (effect time - drives scrolling, morph, rotation)
-- params[3]  = cam_y       (camera height, default 6)
-- params[4]  = obj_x       (object x offset from camera, default 0)
-- params[5]  = obj_y       (object y position, default 4)
-- params[6]  = obj_z       (object z distance from camera, default 13)
-- params[7]  = obj_scale   (object scale, default 2.45)
-- params[8]  = grid_bright (grid brightness 0..1, default 1)
-- params[9]  = grid_warp   (grid lateral warp, default 0)
-- params[10] = peak_master (terrain displacement, default 0)
-- params[11] = fade        (0=black, 1=normal, >1=white)
-- params[12] = shake       (screen shake amplitude in px, 0=off)
-- params[13] = glitch      (glitch intensity 0..1, 0=off)
-- params[14] = obj_morph      (int=shape index 0..N-1, frac=blend to next)
-- params[15] = hz_brightness  (horizon glow multiplier 0..1, default 1)
------------------------------------------------------------

vectorworld = {
  t           = 0,
  cam_y       = 6,
  obj_x       = 0.0,
  obj_y       = 4.0,
  obj_z       = 13.0,
  obj_scale   = 2.45,
  obj_morph   = 0.0,
  grid_bright = 1.0,
  grid_warp   = 0.0,
  peak_master = 0.0,
  fade        = 1.0,
  shake       = 0,
  glitch      = 0,
  hz_brightness = 1.0,

  _pal_dirty  = false,
}


vectorworld.load = function(this)
  vid(3)

  COLS1 = CFG.cols + 1
  ROWS1 = CFG.rows + 1
  grid_world_ud = userdata("f64", 4, COLS1 * ROWS1)
  grid_view_ud = userdata("f64", 4, COLS1 * ROWS1)
  use_matmul3d = (type(grid_world_ud.matmul3d) == "function")

  base_x = {}
  for c=1,COLS1 do
    base_x[c] = (c-1 - CFG.cols * 0.5) * CFG.dx
  end

  load_meshes(MESH_DATA)
end


vectorworld.init = function(this)
  poke(0x5508, 0x3f)
  poke(0x5509, 0x3f)
  poke(0x550a, 0x3f)
  poke(0x550b, 0x00)

  pal()
  palt(0, false)
  fillp()

  set_rgb_ramp()
  build_add_table()

  this._pal_dirty = false
end


vectorworld.setparams = function(this, params)
  this.t           = params[2] or 0
  this.cam_y       = params[3] or 6
  this.obj_x       = params[4] or 0
  this.obj_y       = params[5] or 4
  this.obj_z       = params[6] or 13
  this.obj_scale   = params[7] or 2.45
  if params[8] == nil then this.grid_bright = 1.0 else this.grid_bright = params[8] end
  this.grid_warp   = params[9] or 0
  this.peak_master = params[10] or 0
  this.fade        = params[11] or 1.0
  this.shake       = params[12] or 0
  this.glitch      = params[13] or 0
  this.obj_morph   = params[14] or 0
  if params[15] == nil then this.hz_brightness = 1.0 else this.hz_brightness = params[15] end
end


vectorworld.update = function(this)
  local fade = this.fade
  local needs_transform = fade ~= 1.0

  if needs_transform then
    for i = 0, 63 do
      local c = base_pal[i]
      if c then
        local r = (c >> 16) & 0xff
        local g = (c >> 8) & 0xff
        local b = c & 0xff

        if fade < 1.0 then
          r = r * fade; g = g * fade; b = b * fade
        elseif fade > 1.0 then
          local ft = fade - 1.0
          r = r + (255 - r) * ft; g = g + (255 - g) * ft; b = b + (255 - b) * ft
        end

        r = clamp(flr(r), 0, 255)
        g = clamp(flr(g), 0, 255)
        b = clamp(flr(b), 0, 255)
        pal(i, (r << 16) | (g << 8) | b, 2)
      end
    end
    this._pal_dirty = true
  elseif this._pal_dirty then
    for i = 0, 63 do
      if base_pal[i] then
        pal(i, base_pal[i], 2)
      end
    end
    this._pal_dirty = false
  end
end


vectorworld.draw = function(this)
  -- apply rocket params to CFG
  CFG.cam_y = this.cam_y
  CFG.grid_warp = this.grid_warp
  CFG.peak_master = this.peak_master

  CP = cos(CFG.pitch)
  SP = sin(CFG.pitch)

  if CFG.mesh_dedupe then
    edge_seen = {}
  end

  local t = this.t
  local camz = t * CFG.scroll
  local camx = sin(t * 0.43) * CFG.cam_sway

  -- shared view matrix for grid + mesh
  local vw_tx = -camx
  local vw_ty = -(CFG.cam_y * CP + camz * SP)
  local vw_tz = (CFG.cam_y * SP - camz * CP)
  build_matrix(view_matrix, 0, -CFG.pitch, 0, vw_tx, vw_ty, vw_tz, 1)
  if use_matmul3d then
    view_matrix:transpose(view_matrix_t)
  end

  -- shake: offset drawing origin
  local shake_ox, shake_oy = 0, 0
  if this.shake > 0 then
    local a = this.shake
    shake_ox = flr((irnd(2000) / 1000 - 1) * a)
    shake_oy = flr((irnd(2000) / 1000 - 1) * a)
    camera(-shake_ox, -shake_oy)
  end

  poke(0x550b, 0x00)
  draw_bg(this.hz_brightness)

  build_grid(t, camx, camz)

  poke(0x550b, 0x3f)
  draw_grid(this.grid_bright)
  flush_lines()

  build_object_mesh(t, camx, camz, this.obj_x, this.obj_y, this.obj_z, this.obj_scale, this.obj_morph)
  draw_object_mesh(CFG.obj_core, CFG.obj_halo)
  flush_lines()

  poke(0x550b, 0x00)

  -- reset camera offset
  if shake_ox ~= 0 or shake_oy ~= 0 then
    camera(0, 0)
  end

  -- glitch post-processing: horizontal slice displacement
  if this.glitch > 0 then
    local disp = get_display()
    local amp = this.glitch * 18
    local slices = 24
    local iw, ih = W, H
    local snap = userdata("u8", iw, ih)
    snap:copy(disp, true, 0, 0, iw * ih, 0, 0, 1)
    for s = 0, slices - 1 do
      local sy0 = flr(s * ih / slices)
      local sy1 = flr((s + 1) * ih / slices)
      local sh = sy1 - sy0
      if sh > 0 then
        local off = flr((irnd(2000) / 1000 - 1) * amp)
        if irnd(9) == 0 then off += (irnd(2) * 2 - 1) * flr(amp * 0.9) end
        if off != 0 then
          for y = sy0, sy1 - 1 do
            local row_off = y * iw
            if off > 0 then
              disp:copy(snap, true, row_off, row_off + off, iw - off, 0, 0, 1)
              disp:copy(0, true, 0, row_off, off, 0, 0, 1)
            else
              local a = -off
              disp:copy(snap, true, row_off + a, row_off, iw - a, 0, 0, 1)
              disp:copy(0, true, 0, row_off + iw - a, a, 0, 0, 1)
            end
          end
        end
      end
    end
  end
end