DramaticShapeVoxelMod archival mirror · read-only
tests/voxel_perf_probe.lua

voxel_perf_probe.lua

tests/voxel_perf_probe.lua7.6 KB10064449e2677694
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
-- Driver: diagnose the voxel mode-switch hitch and map-seam artifacts.
--
-- Three passes, all printed:
--   1. Teleport to Route 1, engage voxel, and time every build-side
--      function (Structures, ChunkMesher, meshes, atlases) plus per-frame
--      wall time, so the mode-switch hitch decomposes into named costs.
--   2. Rebuild the seam-band geometry CPU-side and list every quad that
--      rises above the ground plane near the Route 1 / Pallet seam, to
--      name the source of the stray pixels.
--   3. Walk across the seam both ways logging the ground height the
--      voxel pass would stand the player on each frame (the "hop").
return function(game)
  local U = dofile("tests/drivers/util.lua")
  local DIR = os.getenv("SHOT_DIR") or "shots"
  local clock = (love.timer and love.timer.getTime) or os.clock

  U.teleport(game, "ROUTE_1", 10, 30, "down")
  U.wait(10)

  local handle = game.mods.exports["DRAMATIC_SHAPE"]
  if not (handle and handle.lib) then
    print("[perf] DRAMATIC_SHAPE mod not loaded")
    return
  end
  local V = handle.lib
  local Structures = V.require("Structures")
  local ChunkMesher = V.require("ChunkMesher")
  local Voxel3D = V.require("Voxel3D")
  local TerrainAtlas = V.require("TerrainAtlas")
  local TileShape = V.require("TileShape")

  -- ---- pass 1: instrument the build ----
  local stats, order = {}, {}
  local function wrap(tbl, name, label, argLabel)
    local orig = tbl[name]
    if not orig then print("[perf] missing " .. label); return end
    tbl[name] = function(...)
      local t0 = clock()
      local r1, r2, r3 = orig(...)
      local dt = clock() - t0
      local lbl = label
      if argLabel then
        local extra = argLabel(...)
        if extra then lbl = label .. "[" .. tostring(extra) .. "]" end
      end
      local s = stats[lbl]
      if not s then
        s = { n = 0, total = 0, max = 0 }
        stats[lbl] = s
        order[#order + 1] = lbl
      end
      s.n = s.n + 1
      s.total = s.total + dt
      if dt > s.max then s.max = dt end
      return r1, r2, r3
    end
  end
  local mapId = function(map) return map.id end
  wrap(Structures, "forMap", "Structures.forMap", mapId)
  wrap(Structures, "extractObjects", "Structures.extractObjects")
  wrap(Structures, "buildVolume", "Structures.buildVolume")
  wrap(Structures, "buildObject", "Structures.buildObject")
  wrap(Structures, "buildGrass", "Structures.buildGrass")
  wrap(Structures, "buildCylinders", "Structures.buildCylinders")
  wrap(Structures, "buildStairs", "Structures.buildStairs")
  wrap(Structures, "buildBookcases", "Structures.buildBookcases")
  wrap(ChunkMesher, "geometry", "ChunkMesher.geometry", mapId)
  wrap(ChunkMesher, "build", "ChunkMesher.build", mapId)
  wrap(Voxel3D, "newMesh", "Voxel3D.newMesh")
  wrap(TerrainAtlas, "forMap", "TerrainAtlas.forMap", mapId)

  -- engage voxel level 2 (35 degrees) like the player: 6, then 6 again
  local frameTimes = {}
  game:keypressed("6")
  for i = 1, 30 do
    local f0 = clock()
    coroutine.yield()
    frameTimes[i] = clock() - f0
  end
  game:keypressed("6")
  for i = 31, 60 do
    local f0 = clock()
    coroutine.yield()
    frameTimes[i] = clock() - f0
  end
  U.shot(game, DIR .. "/perf_route1_voxel.png")

  local sorted = {}
  for _, lbl in ipairs(order) do sorted[#sorted + 1] = lbl end
  table.sort(sorted, function(a, b) return stats[a].total > stats[b].total end)
  print("[perf] ---- build stats (sorted by total) ----")
  for _, lbl in ipairs(sorted) do
    local s = stats[lbl]
    print(("[perf] %-50s n=%5d total=%8.1fms max=%7.1fms")
      :format(lbl, s.n, s.total * 1000, s.max * 1000))
  end
  print("[perf] ---- frame spikes over 25ms ----")
  for i, ft in ipairs(frameTimes) do
    if ft > 0.025 then
      print(("[perf] frame %02d: %.1fms"):format(i, ft * 1000))
    end
  end

  -- ---- pass 2: seam-band geometry scan ----
  local function scanQuads(tag, verts, ox, oy, zlo, zhi, xlo, xhi)
    local n = 0
    for i = 1, #verts, 4 do
      local minX, minY, minZ = math.huge, math.huge, math.huge
      local maxX, maxY, maxZ = -math.huge, -math.huge, -math.huge
      for j = i, i + 3 do
        local v = verts[j]
        local x, y, z = v[1] + ox, v[2], v[3] + oy
        if x < minX then minX = x end
        if x > maxX then maxX = x end
        if v[2] < minY then minY = y end
        if y > maxY then maxY = y end
        if z < minZ then minZ = z end
        if z > maxZ then maxZ = z end
      end
      if maxY > 0.01 and minZ < zhi and maxZ > zlo
         and (not xlo or (minX < xhi and maxX > xlo)) then
        n = n + 1
        if n <= 60 then
          print(("[seam] %s x[%6.1f..%6.1f] y[%5.1f..%5.1f] z[%6.1f..%6.1f]")
            :format(tag, minX, maxX, minY, maxY, minZ, maxZ))
        end
      end
    end
    print(("[seam] %s: %d raised quads in band"):format(tag, n))
  end
  local function flatQuads(list)
    -- Structures object/grass quad lists ({v1..v4, uv=..} entries) -> verts
    local verts = {}
    for _, q in ipairs(list) do
      for j = 1, 4 do verts[#verts + 1] = q[j] end
    end
    return verts
  end

  local ow = game.overworld
  print("[seam] current map: " .. tostring(ow.map.id))
  local masks = {}
  for _, nb in ipairs(ow.neighbors or {}) do
    masks[#masks + 1] = { nb.ox, nb.oy, nb.ox + nb.map.def.width * 32,
                          nb.oy + nb.map.def.height * 32 }
    print(("[seam] neighbor %-16s ox=%d oy=%d"):format(nb.map.id, nb.ox, nb.oy))
  end
  local seamZ = ow.map.def.height * 32
  local zlo, zhi = seamZ - 24, seamZ + 24
  print(("[seam] scanning band z=[%d..%d] (Route 1 south edge at %d)")
    :format(zlo, zhi, seamZ))
  do
    local verts = ChunkMesher.geometry(ow.map, false, masks)
    scanQuads("cur.full ", verts, 0, 0, zlo, zhi)
    local S = Structures.forMap(ow.map)
    scanQuads("cur.grass", flatQuads(S.grassQuads), 0, 0, zlo, zhi)
    scanQuads("cur.objs ", flatQuads(S.objectQuads), 0, 0, zlo, zhi)
  end
  for _, nb in ipairs(ow.neighbors or {}) do
    local verts = ChunkMesher.geometry(nb.map, true)
    scanQuads("nb." .. nb.map.id, verts, nb.ox, nb.oy, zlo, zhi)
    local S = Structures.forMap(nb.map)
    scanQuads("nb." .. nb.map.id .. ".grass", flatQuads(S.grassQuads),
              nb.ox, nb.oy, zlo, zhi)
  end

  -- ---- pass 3: cross the seam both ways, logging ground height ----
  local function crossLog(fromMap, x, y, dir, steps, tag)
    U.teleport(game, fromMap, x, y, dir)
    U.wait(20)
    print(("[hop] ---- %s: %s (%d,%d) heading %s ----")
      :format(tag, fromMap, x, y, dir))
    for i = 1, steps do
      table.insert(game.input.pressQueue, dir)
      game.input.state[dir] = true
      local f0 = clock()
      coroutine.yield()
      local ft = clock() - f0
      local o = game.overworld
      local p = o.player
      local m = o.map
      local shapes = TileShape.forMap(m)
      local s = shapes[m:cellTile(p.cellX, p.cellY)]
      local g = 0
      if s and s.art ~= "stair" and s.h > 0 then g = s.h end
      if not m:inBounds(p.cellX, p.cellY) or g ~= 0 or ft > 0.025 then
        print(("[hop] f%02d map=%-14s cell=(%d,%d) py=%.0f inB=%s gh=%d cls=%s ft=%.0fms")
          :format(i, m.id, p.cellX, p.cellY, p.py,
                  tostring(m:inBounds(p.cellX, p.cellY)), g,
                  s and s.class or "?", ft * 1000))
      end
      game.input.state[dir] = false
    end
    local o = game.overworld
    print(("[hop] end map=%s cell=(%d,%d)")
      :format(o.map.id, o.player.cellX, o.player.cellY))
  end
  crossLog("ROUTE_1", 10, 34, "down", 60, "route1 -> pallet")
  U.shot(game, DIR .. "/perf_pallet_after_cross.png")
  crossLog("PALLET_TOWN", 10, 1, "up", 60, "pallet -> route1")
  U.shot(game, DIR .. "/perf_route1_after_cross.png")

  print("[perf] done")
end