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.

@sim - my apologies capturing the error text is available now in V4, and works well.

@sim i also think there is some sort of memory leak going on. I can run my program perfectly, then the second time it might crash and exit codea completely. Then next run it will exit codea again, then the next time it will work. I sent you a couple of crash logs.

@sim for the first time ever i got an error message in the side panel in red…’not enough memory’. I guess somehow not all the memory gets released after a run?