Memory leak between runs

I’m trying to make an endless runner using voxels.

This code runs fine on my iPhone 16e once.

The second time I run it, the entire Codea app crashes fully almost immediately.

There’s apparently a memory leak happening between runs, right? Something Codea doesn’t fully clean up when a project quits to the editor.

  
--# Main
require(asset.documents.Craft.Cameras)
require(asset.documents.Craft.Block_Library)

function setup()
  collectgarbage("collect")
  scene = craft.scene()
  blocks()
  
  scene.sun.rotation = quat.eulerAngles(25, 125, 0)
  scene.ambientColor = color(96, 96, 96, 255)
  scene.sun:get(craft.light).intensity = 0.6
  skyColor = color(0, 134, 255, 255)
  scene.fogEnabled = true
  scene.fogColor = skyColor
  
  grassID = scene.voxels.blocks.grass.id
  dirtID  = scene.voxels.blocks.dirt.id
  
  VOL_W  = 48
  VOL_H  = 10
  VOL_D  = 16
  AHEAD  = 9
  BEHIND = 2
  BASE   = 2     -- lowest surface height inside a volume
  HILL   = 6     -- height variation above BASE  (BASE+HILL must be < VOL_H)
  
  heightNoise = craft.noise.perlin()
  heightNoise.octaves = 3
  heightNoise.frequency = 0.02
  heightNoise.seed = 1
  
  scene.fogNear = AHEAD * VOL_D * 0.6
  scene.fogFar  = AHEAD * VOL_D
  
  runner = vec3(VOL_W/2 + 0.5, BASE + 1, BEHIND * VOL_D)
  footY  = sampleHeight(math.floor(runner.x), math.floor(runner.z)) + 1
  footVY = 0
  camGroundY = footY
  
  pool = {}
  for i = 0, AHEAD + BEHIND - 1 do
    local e = scene:entity()
    e.position = vec3(0, 0, i * VOL_D)
    local v = e:add(craft.volume, VOL_W, VOL_H, VOL_D)
    fillTerrain(v, i * VOL_D)
    table.insert(pool, { entity = e, volume = v, row = i })
  end
  parameter.watch("memKB")     -- add in setup
  
  setupWalkParameters()
  ArmSwing = 72; LegSwing = 75; WalkSpeed = 10
  rig = adventurerRig(scene)
  
  parameter.number("RunSpeed",    0, 40, 40)
  parameter.number("CamHeight",   2, 100, 13.56)
  parameter.number("CamBack",     2, 100, 20)
  parameter.number("CamPitch",  -90, 90, 17.37)
  parameter.number("CamYaw",   -180, 180, 0)
  parameter.number("CamSmooth",   0, 10, 1.5)
  parameter.number("StepUp",      0, 30, 12)
  parameter.number("Gravity",     0, 80, 30)
  parameter.number("AvatarScale", 0.01, 8, 0.1)
  parameter.number("AvatarLift",  -2, 10, 0.2)
  parameter.number("AvatarYaw",  -180, 180, 180)
end

-- surface height at a world cell (same function the avatar's feet use)
function sampleHeight(wx, wz)
  local n = heightNoise:getValue(wx, 0, wz)        -- ~ -1..1
  return BASE + math.floor((n + 1) * 0.5 * HILL)
end

-- thin shell: grass at the surface, 2 dirt below, empty elsewhere
function fillTerrain(v, wz0)
  for x = 0, VOL_W-1 do
    for z = 0, VOL_D-1 do
      local h = sampleHeight(x, wz0 + z)
      for y = 0, VOL_H-1 do
        if y == h then
          v:set(x, y, z, BLOCK_ID, grassID)
        elseif y >= h-2 and y < h then
          v:set(x, y, z, BLOCK_ID, dirtID)
        else
          v:set(x, y, z, BLOCK_ID, 0)   -- clears old content on recycle
        end
      end
    end
  end
end

function update(dt)
  runner.z = runner.z + RunSpeed * dt
  
  local maxRow = -1e9
  for _, p in ipairs(pool) do
    if p.row > maxRow then maxRow = p.row end
  end
  
  local behindZ = runner.z - BEHIND * VOL_D
  for _, p in ipairs(pool) do
    if (p.row * VOL_D) + VOL_D < behindZ then
      maxRow = maxRow + 1
      p.row = maxRow
      p.entity.position = vec3(0, 0, p.row * VOL_D)
      fillTerrain(p.volume, p.row * VOL_D)   -- refill with terrain at the new spot
      -- in update, inside the recycle loop, right after fillTerrain(...):
      collectgarbage("collect")
      
    end
  end
  
  -- ground under the runner, from the same noise
  local groundY = sampleHeight(math.floor(runner.x), math.floor(runner.z)) + 1
  
  -- step up quickly, fall under gravity going down
  if groundY > footY then
    footY = footY + (groundY - footY) * (1 - math.exp(-StepUp * dt))
    footVY = 0
  else
    footVY = footVY - Gravity * dt
    footY  = footY + footVY * dt
    if footY <= groundY then footY = groundY; footVY = 0 end
  end
  
  camGroundY = camGroundY + (groundY - camGroundY) * (1 - math.exp(-CamSmooth * dt))
  
  rig.root.position = vec3(runner.x, footY + AvatarLift, runner.z)
  rig.root.rotation = quat.eulerAngles(0, AvatarYaw, 0)
  rig.root.scale    = vec3(AvatarScale, AvatarScale, AvatarScale)
  
  scene.camera.position = vec3(runner.x, camGroundY + CamHeight, runner.z - CamBack)
  scene.camera.rotation = quat.eulerAngles(CamPitch, CamYaw, 0)
  
  animateWalk(rig, ElapsedTime)
  scene:update(dt)
  -- at the very end of update:
  memKB = math.floor(collectgarbage("count"))
end

function draw()
  update(DeltaTime)
  scene:draw()
end

--# Avatar
BODY_PARTS = {
  head     = {13,14,15,16,17,18,19,20,21,22,23,24},
  torso    = {1,2,3,4,5,6,7,8,9,10,11,12,49,52},
  leftArm  = {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},
  rightArm = {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},
  leftLeg  = {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},
  rightLeg = {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}
}

function setupWalkParameters()
  local function add(name, minV, maxV, defaultV)
    local startValue = readLocalData(name, defaultV)
    _G[name] = startValue
    parameter.number(name, minV, maxV, startValue, function(v)
      _G[name] = v
      saveLocalData(name, v)
    end)
  end
  add("ArmPivotX", 0, 1, 0.38)
  add("ArmPivotY", -1, 15, 10.66)
  add("ArmPivotZ", -1, 1, 0)
  add("LegPivotX", 0, 1, 0.18)
  add("LegPivotY", -1, 15, 6.86)
  add("LegPivotZ", -1, 1, 0)
  add("ArmSwing", 0, 90, 31.20)
  add("LegSwing", 0, 90, 38.95)
  add("WalkSpeed", 0, 10, 3)
end

function adventurerRig(scene)
  local model = craft.model(asset.builtin.Blocky_Characters.Adventurer)
  local material = craft.material(asset.builtin.Materials.Standard)
  material.map = readImage(asset.builtin.Blocky_Characters.AdventurerSkin)
  
  local ind = model.indices
  local triangles = {}
  for i = 1, #ind, 3 do
    triangles[#triangles+1] = { i1 = ind[i], i2 = ind[i+1], i3 = ind[i+2] }
  end
  
  local function buildMesh(triIndices)
    local pos, uv, col, norm = model.positions, model.uvs, model.colors, model.normals
    local newPos, newUV, newCol, newNorm, newInd, vertexMap = {}, {}, {}, {}, {}, {}
    local function addVertex(old)
      if vertexMap[old] then return vertexMap[old] end
      local new = #newPos + 1
      vertexMap[old] = new
      newPos[new] = pos[old]
      if uv   then newUV[new]   = uv[old]   end
      if col  then newCol[new]  = col[old]  end
      if norm then newNorm[new] = norm[old] end
      return new
    end
    for _, triId in ipairs(triIndices) do
      local t = triangles[triId]
      if t then
        table.insert(newInd, addVertex(t.i1))
        table.insert(newInd, addVertex(t.i2))
        table.insert(newInd, addVertex(t.i3))
      end
    end
    local m = craft.model()
    m.positions = newPos
    m.indices = newInd
    if #newUV   > 0 then m.uvs     = newUV   end
    if #newCol  > 0 then m.colors  = newCol  end
    if #newNorm > 0 then m.normals = newNorm end
    return m
  end
  
  local root = scene:entity()
  
  local torsoPivot = scene:entity();    torsoPivot.parent = root
  local headPivot = scene:entity();     headPivot.parent = torsoPivot
  local leftArmPivot = scene:entity();  leftArmPivot.parent = torsoPivot
  local rightArmPivot = scene:entity(); rightArmPivot.parent = torsoPivot
  local leftLegPivot = scene:entity();  leftLegPivot.parent = torsoPivot
  local rightLegPivot = scene:entity(); rightLegPivot.parent = torsoPivot
  
  local meshes = {}
  for name, triIndices in pairs(BODY_PARTS) do
    local e = scene:entity()
    e.model = buildMesh(triIndices)
    e.material = material
    meshes[name] = e
  end
  
  meshes.torso.parent    = torsoPivot
  meshes.head.parent     = headPivot
  meshes.leftArm.parent  = leftArmPivot
  meshes.rightArm.parent = rightArmPivot
  meshes.leftLeg.parent  = leftLegPivot
  meshes.rightLeg.parent = rightLegPivot
  
  leftArmPivot.position  = vec3( 0.38, 0.40, 0)
  rightArmPivot.position = vec3(-0.38, 0.40, 0)
  leftLegPivot.position  = vec3( 0.18,-0.50, 0)
  rightLegPivot.position = vec3(-0.18,-0.50, 0)
  headPivot.position     = vec3(0,0.72,0)
  
  meshes.leftArm.position  = -leftArmPivot.position
  meshes.rightArm.position = -rightArmPivot.position
  meshes.leftLeg.position  = -leftLegPivot.position
  meshes.rightLeg.position = -rightLegPivot.position
  meshes.head.position     = -headPivot.position
  
  return {
    root = root,
    torsoPivot = torsoPivot, headPivot = headPivot,
    leftArmPivot = leftArmPivot, rightArmPivot = rightArmPivot,
    leftLegPivot = leftLegPivot, rightLegPivot = rightLegPivot,
    meshes = meshes
  }
end

function animateWalk(rig, t)
  local s = math.sin(t * WalkSpeed)
  
  rig.leftArmPivot.position  = vec3( ArmPivotX, ArmPivotY, ArmPivotZ )
  rig.rightArmPivot.position = vec3(-ArmPivotX, ArmPivotY, ArmPivotZ )
  rig.leftLegPivot.position  = vec3( LegPivotX, LegPivotY, LegPivotZ )
  rig.rightLegPivot.position = vec3(-LegPivotX, LegPivotY, LegPivotZ )
  
  rig.meshes.leftArm.position  = -rig.leftArmPivot.position
  rig.meshes.rightArm.position = -rig.rightArmPivot.position
  rig.meshes.leftLeg.position  = -rig.leftLegPivot.position
  rig.meshes.rightLeg.position = -rig.rightLegPivot.position
  rig.meshes.head.position     = -rig.headPivot.position
  
  rig.leftArmPivot.rotation  = quat.eulerAngles( s * ArmSwing, 0, 0)
  rig.rightArmPivot.rotation = quat.eulerAngles(-s * ArmSwing, 0, 0)
  rig.leftLegPivot.rotation  = quat.eulerAngles(-s * LegSwing, 0, 0)
  rig.rightLegPivot.rotation = quat.eulerAngles( s * LegSwing, 0, 0)
end




@UberGoober - can’t get this to run at all on my iPad. Several errors, seem to be related to Craft in setup() referring to craft.scene().

@sim , @jfperusse - suggestion, tried to capture part of the error report on @UberGoober’s project I can get the selection pointers up in the error window but they are not manageable (other than scrollable) nor respond to trying to copy.

If it’s not there already I suggest an “Error Capture” button to save a file (under a delegated number and possibly user name/ID) so that we can post sections on the net/forums. Could do this by screen capture but that’s a bit messy.