Garbage Collector and FFI

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
User avatar
UnixRoot
Party member
Posts: 100
Joined: Mon Nov 08, 2021 8:10 am

Garbage Collector and FFI

Post by UnixRoot »

Hi guys,

I have a problem with the garbage collector and my FFI structs and arrays. As soon as I use any FFI stuff, my program crashes instantly or my textures get corrupted. If I disable the garbage collector, the program runs just fine and doesn't even produce any noteworthy garbage. It stays at around 75 MB for hours.

Here are a few examples of my FFI stuff:

My frame buffer and depth buffer

Code: Select all

local framebuffer = love.image.newImageData(width, height, "rgb565")
local depthbuffer = love.image.newImageData(width, height, "r32f")

local fb = ffi.cast("uint16_t*", framebuffer:getFFIPointer())
local db = ffi.cast("float*",depthbuffer:getFFIPointer())
The only texture I'm using right now

Code: Select all

local texdata = parseBMP("gimp.bmp")
local tex = ffi.cast("uint16_t*", texdata:getFFIPointer())
This is how it destroys my textures, loaded with the code above
garbage.png
garbage.png (56.63 KiB) Viewed 3426 times
Or my OBJ loader stuff

Code: Select all

ffi.cdef[[
typedef struct {
    float x, y, z, w;
    float u, v;
} Vertex;

typedef struct {
    int v1, v2, v3;
    int vt1, vt2, vt3;
} Face;

typedef struct {
    int num_vertices;
    int num_faces;
    Vertex* vertices;
    Face* faces;
   
} OBJModel;
]]

    local model = ffi.new("OBJModel")
    model.vertices = ffi.new("Vertex[?]", model.num_vertices)
    model.faces = ffi.new("Face[?]", model.num_faces)
I don't create new objects all the time, I reuse what I have, I don't even get any garbage, but it crashes or destroys my textures or vertices as soon as the garbage collector is on.

Without the garbage collector, everything is fine
good.PNG
good.PNG (20.9 KiB) Viewed 3414 times
User avatar
slime
Solid Snayke
Posts: 3166
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Garbage Collector and FFI

Post by slime »

Make sure the original memory allocations (for example the objects returned by love.image.newImageData in your first bit of code) are still accessible when the FFI pointers that point to their memory are used. Otherwise Lua may garbage-collect the original objects and free their memory while you're using pointers to that memory.
User avatar
UnixRoot
Party member
Posts: 100
Joined: Mon Nov 08, 2021 8:10 am

Re: Garbage Collector and FFI

Post by UnixRoot »

Good point, but that's probably not the case. I don't delete any of those objects while still in use. I mean, it crashes instantly.
User avatar
slime
Solid Snayke
Posts: 3166
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Garbage Collector and FFI

Post by slime »

The objects can just be garbage collected because they're recognized as not being used in the future, they don't have to be manually deleted for the GC to clean them up.

Also make sure you're using zero-based indexing with FFI arrays.

We can only throw guesses at the wall with how little information you've provided. :)
User avatar
UnixRoot
Party member
Posts: 100
Joined: Mon Nov 08, 2021 8:10 am

Re: Garbage Collector and FFI

Post by UnixRoot »

slime wrote: Tue May 07, 2024 5:36 pm The objects can just be garbage collected because they're recognized as not being used in the future
And how can I change that?
slime wrote: Tue May 07, 2024 5:36 pm Also make sure you're using zero-based indexing with FFI arrays.
Yup, I'm aware of that
slime wrote: Tue May 07, 2024 5:36 pm We can only throw guesses at the wall with how little information you've provided. :)
What else do you need?
User avatar
slime
Solid Snayke
Posts: 3166
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Garbage Collector and FFI

Post by slime »

UnixRoot wrote: Tue May 07, 2024 6:04 pm
slime wrote: Tue May 07, 2024 5:36 pm The objects can just be garbage collected because they're recognized as not being used in the future
And how can I change that?
Reference/use the object after you've used the FFI pointer. See https://luajit.org/ext_ffi_semantics.html#gc for some details.
UnixRoot wrote: Tue May 07, 2024 6:04 pm
slime wrote: Tue May 07, 2024 5:36 pm We can only throw guesses at the wall with how little information you've provided. :)
What else do you need?
Posting all relevant code (including everywhere the original allocations and the ffi pointers are referenced or used) or uploading a .love file would help
User avatar
UnixRoot
Party member
Posts: 100
Joined: Mon Nov 08, 2021 8:10 am

Re: Garbage Collector and FFI

Post by UnixRoot »

slime wrote: Tue May 07, 2024 7:47 pm Posting all relevant code (including everywhere the original allocations and the ffi pointers are referenced or used) or uploading a .love file would help
Here we go:

Code: Select all

local vec3 = require("math/vec3")
local mat4 = require("math/mat4")
local bmpParser = require("bmp_parser")
local objLoader = require("obj_loader/obj_loader")

-- Window dimensions
local WINDOW_WIDTH, WINDOW_HEIGHT = 640, 360

-- Import commonly used math functions
local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
local bor, band, lshift, rshift = bit.bor, bit.band, bit.lshift, bit.rshift
local rad = math.rad

-- Load the FFI library to work with C-style data structures
local ffi = require("ffi")

-- Define the framebuffer and depthbuffer data structures
ffi.cdef[[
    typedef struct {
        uint16_t* pixels;
    } FramebufferData;

    typedef struct {
        float* depth;
    } DepthbufferData;
]]

-- Create a table to store the framebuffer, depthbuffer, and texture data
local textureData = {
   framebuffer = love.image.newImageData(WINDOW_WIDTH, WINDOW_HEIGHT, "rgb565"),
   depthbuffer = love.image.newImageData(WINDOW_WIDTH, WINDOW_HEIGHT, "r32f"),
   textureData = bmpParser.parse("gimp.bmp")
}

-- Get pointers to the pixel data using FFI
local framebufferPtr = ffi.cast("uint16_t*", textureData.framebuffer:getFFIPointer())
local depthbufferPtr = ffi.cast("float*", textureData.depthbuffer:getFFIPointer())
local texturePtr = ffi.cast("uint16_t*", textureData.textureData:getFFIPointer())

-- Set the default filter for the framebuffer image
love.graphics.setDefaultFilter("nearest", "nearest", 0)

-- Create the framebuffer Image object
local framebufferImage = love.graphics.newImage(textureData.framebuffer)

-- Clear the framebuffer and depthbuffer with a specific color and depth value
local function clearFramebuffer(color)
  for i = 0, WINDOW_WIDTH * WINDOW_HEIGHT - 1 do
    framebufferPtr[i] = color
    depthbufferPtr[i] = -1e6
  end
end

-- Refresh the framebuffer Image with the latest pixel data
local function refreshFramebuffer()
  framebufferImage:replacePixels(textureData.framebuffer)
end

-- Calculate the bounding box of a triangle
local function computeBoundingBox(x1, y1, x2, y2, x3, y3, width, height)
  local minX = min(x1, x2, x3)
  local minY = min(y1, y2, y3)
  local maxX = max(x1, x2, x3)
  local maxY = max(y1, y2, y3)

  -- Ensure the bounding box is within the screen bounds
  minX = max(minX, 0)
  minY = max(minY, 0)
  maxX = min(maxX, width - 1)
  maxY = min(maxY, height - 1)

  return floor(minX), floor(minY), floor(maxX), floor(maxY)
end

-- Rasterize a triangle and update the framebuffer and depthbuffer
local function rasterizeTriangle(x1, y1, z1, u1, v1, x2, y2, z2, u2, v2, x3, y3, z3, u3, v3, texturePtr, framebufferPtr, depthbufferPtr, width, height)
  local minX, minY, maxX, maxY = computeBoundingBox(x1, y1, x2, y2, x3, y3, width, height)

  local recip_z1 = 1 / z1
  local recip_z2 = 1 / z2
  local recip_z3 = 1 / z3

  for y = minY, maxY, 1 do
    for x = minX, maxX do
      local denom = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3)
      local w1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / denom
      local w2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / denom
      local w3 = 1.0 - w1 - w2

      -- Check if the pixel is inside the triangle
      if w1 >= 0 and w2 >= 0 and w3 >= 0 then
        -- Calculate the depth of the current pixel
        local depth = w1 * recip_z1 + w2 * recip_z2 + w3 * recip_z3
        local index = y * width + x

        -- Check if the current pixel is closer than the one in the Z-buffer
        if depth > depthbufferPtr[index] then
          -- Calculate UV coordinates
          local u = max(1, min(128, floor((w1 * u1 + w2 * u2 + w3 * u3) % 128)))
          local v = max(1, min(128, floor((w1 * v1 + w2 * v2 + w3 * v3) % 128)))

          -- Update the framebuffer and the Z-buffer
          framebufferPtr[index] = texturePtr[bor(lshift(v, 7), u)]
          depthbufferPtr[index] = depth
        end
      end
    end
  end
end

-- Perform backface culling to skip triangles facing away from the camera
local function isBackfaceCulled(x1, y1, z1, x2, y2, z2, x3, y3, z3)
  -- Calculate the triangle normal
  local edge1_x = x2 - x1
  local edge1_y = y2 - y1
  local edge2_x = x3 - x1
  local edge2_y = y3 - y1

  -- Calculate the normal vector
  local normalZ = edge1_x * edge2_y - edge1_y * edge2_x

  -- Check if the triangle is facing away from the camera
  if normalZ < 0 then
    return true -- Backface, skip the triangle
  end

  return false -- Triangle is visible, process it
end

-- Transform a vertex using the final transformation matrix
local function transformVertex(finalMatrix, vertex)
  local x, y, z = mat4_multiplyVector(finalMatrix, vertex.x, vertex.y, vertex.z)
  return x, y, z
end

-- Load the 3D model
local model = objLoader.load("test.obj")

-- Set up the view and projection matrices
local NEAR, FAR = 0.1, 1000
local aspect = WINDOW_WIDTH / WINDOW_HEIGHT
local viewMatrix, projectionMatrix, modelMatrix, finalMatrix = mat4_new(), mat4_new(), mat4_new(), mat4_new()

-- Set up the view matrix
mat4_pointAt(0, -0.5, -2, 0, 0, 0, 0, 1, 0, viewMatrix)

-- Set up the projection matrix
mat4_makeProjection(rad(60), aspect, NEAR, FAR, projectionMatrix)

local rotation = 0

-- Update function called every frame
function love.update(dt)
  -- Clear the framebuffer
  clearFramebuffer(0x008F)

  -- Iterate through the triangles of the 3D model
  for i = 0, model.numFaces - 1, 1 do
    -- Rotate the model
    rotation = rotation + dt * 0.01
    mat4_makeRotationY(rad(rotation), modelMatrix)

    -- Combine the projection, view, and model matrices
    mat4_multiply(projectionMatrix, viewMatrix, finalMatrix)
    mat4_multiply(finalMatrix, modelMatrix, finalMatrix)

    -- Transform the vertices using the final matrix
    local x1, y1, z1 = transformVertex(finalMatrix, model.vertices[model.faces[i].v1])
    local x2, y2, z2 = transformVertex(finalMatrix, model.vertices[model.faces[i].v2])
    local x3, y3, z3 = transformVertex(finalMatrix, model.vertices[model.faces[i].v3])

    -- Perform backface culling
    if not isBackfaceCulled(x1, y1, z1, x2, y2, z2, x3, y3, z3) then
      -- Rasterize the triangle
      rasterizeTriangle(
        (x1 + 1) * 0.5 * (WINDOW_WIDTH - 1), (y1 + 1) * 0.5 * (WINDOW_HEIGHT - 1), z1,
        model.textureCoords[model.faces[i].vt1].u * 128, model.textureCoords[model.faces[i].vt1].v * 128,
        (x2 + 1) * 0.5 * (WINDOW_WIDTH - 1), (y2 + 1) * 0.5 * (WINDOW_HEIGHT - 1), z2,
        model.textureCoords[model.faces[i].vt2].u * 128, model.textureCoords[model.faces[i].vt2].v * 128,
        (x3 + 1) * 0.5 * (WINDOW_WIDTH - 1), (y3 + 1) * 0.5 * (WINDOW_HEIGHT - 1), z3,
        model.textureCoords[model.faces[i].vt3].u * 128, model.textureCoords[model.faces[i].vt3].v * 128,
        texturePtr, framebufferPtr, depthbufferPtr, WINDOW_WIDTH, WINDOW_HEIGHT
      )
    end
  end

  -- Refresh the framebuffer Image
  refreshFramebuffer()

  -- Set the window title to display the current FPS
  love.window.setTitle("FPS : " .. love.timer.getFPS())
end

-- Draw the framebuffer Image to the screen
function love.draw()
  love.graphics.draw(framebufferImage, 0, 0)
end

RNavega
Party member
Posts: 385
Joined: Sun Aug 16, 2020 1:28 pm

Re: Garbage Collector and FFI

Post by RNavega »

If you add a print('Function [X] START') and print('Function [X] END'), with [X] being the function name, at the start and end of the body of each function in your code, and run your program with lovec.exe or with "love.exe --console" so the console appears, inside what function does it crash? (the function won't have an END message).
User avatar
pgimeno
Party member
Posts: 3672
Joined: Sun Oct 18, 2015 2:58 pm

Re: Garbage Collector and FFI

Post by pgimeno »

You haven't provided a runnable version that can be used to debug. I've made your code runnable by applying this patch:

Code: Select all

--- ffiorig.lua	2024-05-11 09:47:53.683996392 +0200
+++ ffimod.lua	2024-05-11 09:49:00.158667324 +0200
@@ -1,7 +1,23 @@
-local vec3 = require("math/vec3")
-local mat4 = require("math/mat4")
-local bmpParser = require("bmp_parser")
-local objLoader = require("obj_loader/obj_loader")
+--local vec3 = require("math/vec3")
+--local mat4 = require("math/mat4")
+--local bmpParser = require("bmp_parser")
+--local objLoader = require("obj_loader/obj_loader")
+local function mat4_new() return {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} end
+local function mat4_multiplyVector() return 1, 2, 3 end
+local function null() end
+local mat4_pointAt = null
+local mat4_makeProjection = null
+local mat4_makeRotationY = null
+local mat4_multiply = null
+local bmpParser = { parse = function() return love.image.newImageData(2,2) end }
+local objLoader = {}
+function objLoader.load()
+  return { numFaces = 1,
+    vertices = {{x=1, y=1, z=1}, {x=2, y=2, z=2}, {x=1, y=3, z=3}},
+    faces = {[0]={v1 = 1, v2 = 2, v3 = 3, vt1 = 1, vt2 = 1, vt3 = 1}},
+    textureCoords = {{ u = 0, v = 0 }}
+  }
+end
 
 -- Window dimensions
 local WINDOW_WIDTH, WINDOW_HEIGHT = 640, 360
With this patch, I get no crash whatsoever. Could you provide a complete, self-contained test case that crashes?
User avatar
UnixRoot
Party member
Posts: 100
Joined: Mon Nov 08, 2021 8:10 am

Re: Garbage Collector and FFI

Post by UnixRoot »

I paused the project and got back to another unfinished project I had lying around. And you know what? It's the same thing.

Code: Select all

    local imagedata=love.image.newImageData(640,360, "rgba8")
    local image = love.graphics.newImage(imagedata)
    local terrainBuffer  = ffi.cast('uint32_t*', imagedata:getFFIPointer())

    local imagedata2 = love.image.newImageData(640,360, "rgba8")
    local image2 = love.graphics.newImage(imagedata2)
    local backgroundBuffer = ffi.cast('uint32_t*', imagedata2:getFFIPointer())

    local texdata = love.image.newImageData("tex.png")
    local texBuffer = ffi.cast('uint32_t*', texdata:getFFIPointer())
This code is enough to crash every project. Everything gets garbage collected, the texture, the framebuffers, everything. I really don't know what to do to stop this nonsense.
Post Reply

Who is online

Users browsing this forum: No registered users and 12 guests