diff --git a/FlexLove.lua b/FlexLove.lua index 46d919d..e0eed88 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -12,6 +12,7 @@ local Context = req("Context") local StateManager = req("StateManager") local ErrorCodes = req("ErrorCodes") local ErrorHandler = req("ErrorHandler") +local Performance = req("Performance") local ImageRenderer = req("ImageRenderer") local ImageScaler = req("ImageScaler") local NinePatch = req("NinePatch") @@ -95,7 +96,7 @@ Color.initializeErrorHandler(ErrorHandler) utils.initializeErrorHandler(ErrorHandler) -- Add version and metadata -flexlove._VERSION = "0.2.2" +flexlove._VERSION = "0.2.3" flexlove._DESCRIPTION = "0I Library for LÖVE Framework based on flexbox" flexlove._URL = "https://github.com/mikefreno/FlexLove" flexlove._LICENSE = [[ @@ -122,12 +123,27 @@ flexlove._LICENSE = [[ SOFTWARE. ]] +-- GC (Garbage Collection) configuration +flexlove._gcConfig = { + strategy = "auto", -- "auto", "periodic", "manual", "disabled" + memoryThreshold = 100, -- MB before forcing GC + interval = 60, -- Frames between GC steps (for periodic mode) + stepSize = 200, -- Work units per GC step (higher = more aggressive) +} +flexlove._gcState = { + framesSinceLastGC = 0, + lastMemory = 0, + gcCount = 0, +} + +-- Deferred callback queue for operations that cannot run while Canvas is active +flexlove._deferredCallbacks = {} + --- Initialize FlexLove with configuration options, set refence scale for autoscaling on window resize, immediate mode, and error logging / error file path ----@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean} +---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean, performanceMonitoring?: boolean, performanceWarnings?: boolean, performanceHudKey?: string, performanceHudPosition?: {x: number, y: number} } function flexlove.init(config) config = config or {} - -- Configure error logging if config.errorLogFile then ErrorHandler.setLogTarget("file") ErrorHandler.setLogFile(config.errorLogFile) @@ -137,6 +153,43 @@ function flexlove.init(config) ErrorHandler.setLogFile("flexlove-errors.log") end + -- Configure performance monitoring (default: true) + local enablePerfMonitoring = config.performanceMonitoring + if enablePerfMonitoring == nil then + enablePerfMonitoring = true + end + if enablePerfMonitoring then + Performance.enable() + else + Performance.disable() + end + + local enablePerfWarnings = config.performanceWarnings or true + + Performance.setConfig("warningsEnabled", enablePerfWarnings) + if enablePerfWarnings then + Performance.setConfig("logWarnings", true) + end + + -- Configure performance HUD toggle key (default: "f3") + if config.performanceHudKey then + Performance.setConfig("hudToggleKey", config.performanceHudKey) + end + + -- Configure performance HUD position (default: {x = 10, y = 10}) + if config.performanceHudPosition then + Performance.setConfig("hudPosition", config.performanceHudPosition) + end + + -- Configure memory profiling (default: false) + if config.memoryProfiling then + Performance.enableMemoryProfiling() + -- Register key tables for leak detection + Performance.registerTableForMonitoring("StateManager.stateStore", StateManager._getInternalState().stateStore) + Performance.registerTableForMonitoring("StateManager.stateMetadata", StateManager._getInternalState().stateMetadata) + Performance.registerTableForMonitoring("FONT_CACHE", utils.FONT_CACHE) + end + if config.baseScale then flexlove.baseScale = { width = config.baseScale.width or 1920, @@ -171,6 +224,20 @@ function flexlove.init(config) flexlove._autoFrameManagement = config.autoFrameManagement or false + -- Configure GC strategy + if config.gcStrategy then + flexlove._gcConfig.strategy = config.gcStrategy + end + if config.gcMemoryThreshold then + flexlove._gcConfig.memoryThreshold = config.gcMemoryThreshold + end + if config.gcInterval then + flexlove._gcConfig.interval = config.gcInterval + end + if config.gcStepSize then + flexlove._gcConfig.stepSize = config.gcStepSize + end + if config.stateRetentionFrames or config.maxStateEntries then StateManager.configure({ stateRetentionFrames = config.stateRetentionFrames, @@ -179,6 +246,45 @@ function flexlove.init(config) end end +--- Queue a callback to be executed after the current frame's canvas operations complete +--- This is necessary for operations that cannot run while a Canvas is active (e.g., love.window.setMode) +---@param callback function The callback to execute +function flexlove.deferCallback(callback) + if type(callback) ~= "function" then + ErrorHandler.warn("FlexLove", "deferCallback expects a function") + return + end + table.insert(flexlove._deferredCallbacks, callback) +end + +--- Execute all deferred callbacks and clear the queue +--- IMPORTANT: This MUST be called at the very end of love.draw() after ALL canvases +--- have been released, including any canvases created by the application (not just FlexLove's canvases) +--- @usage +--- function love.draw() +--- love.graphics.setCanvas(myCanvas) +--- FlexLove.draw() +--- love.graphics.setCanvas() -- Release ALL canvases +--- FlexLove.executeDeferredCallbacks() -- Now safe to execute +--- end +function flexlove.executeDeferredCallbacks() + if #flexlove._deferredCallbacks == 0 then + return + end + + -- Copy callbacks and clear queue before execution + -- This prevents infinite loops if callbacks defer more callbacks + local callbacks = flexlove._deferredCallbacks + flexlove._deferredCallbacks = {} + + for _, callback in ipairs(callbacks) do + local success, err = xpcall(callback, debug.traceback) + if not success then + ErrorHandler.warn("FlexLove", string.format("Deferred callback failed: %s", tostring(err))) + end + end +end + function flexlove.resize() local newWidth, newHeight = love.window.getMode() @@ -189,6 +295,14 @@ function flexlove.resize() Blur.clearCache() + -- Release old canvases explicitly + if flexlove._gameCanvas then + flexlove._gameCanvas:release() + end + if flexlove._backdropCanvas then + flexlove._backdropCanvas:release() + end + flexlove._gameCanvas = nil flexlove._backdropCanvas = nil flexlove._canvasDimensions = { width = 0, height = 0 } @@ -230,6 +344,9 @@ function flexlove.beginFrame() return end + -- Start performance frame timing + Performance.startFrame() + flexlove._frameNumber = flexlove._frameNumber + 1 StateManager.incrementFrame() flexlove._currentFrameElements = {} @@ -301,6 +418,10 @@ function flexlove.endFrame() StateManager.cleanup() StateManager.forceCleanupIfNeeded() flexlove._frameStarted = false + + -- End performance frame timing + Performance.endFrame() + Performance.resetFrameCounters() end flexlove._gameCanvas = nil @@ -322,6 +443,14 @@ function flexlove.draw(gameDrawFunc, postDrawFunc) local width, height = love.graphics.getDimensions() if not flexlove._gameCanvas or flexlove._canvasDimensions.width ~= width or flexlove._canvasDimensions.height ~= height then + -- Release old canvases before creating new ones + if flexlove._gameCanvas then + flexlove._gameCanvas:release() + end + if flexlove._backdropCanvas then + flexlove._backdropCanvas:release() + end + flexlove._gameCanvas = love.graphics.newCanvas(width, height) flexlove._backdropCanvas = love.graphics.newCanvas(width, height) flexlove._canvasDimensions.width = width @@ -404,7 +533,15 @@ function flexlove.draw(gameDrawFunc, postDrawFunc) postDrawFunc() end + -- Render performance HUD if enabled + Performance.renderHUD() + love.graphics.setCanvas(outerCanvas) + + -- NOTE: Deferred callbacks are NOT executed here because the calling code + -- (e.g., main.lua) might still have a canvas active. Callbacks must be + -- executed by calling FlexLove.executeDeferredCallbacks() at the very end + -- of love.draw() after ALL canvases have been released. end ---@param element Element @@ -519,6 +656,12 @@ end ---@param dt number function flexlove.update(dt) + -- Update Performance module with actual delta time for accurate FPS + Performance.updateDeltaTime(dt) + + -- Garbage collection management + flexlove._manageGC() + local mx, my = love.mouse.getPosition() local topElement = flexlove.getElementAtPosition(mx, my) @@ -551,6 +694,86 @@ function flexlove.update(dt) end end +--- Internal GC management function (called from update) +function flexlove._manageGC() + local strategy = flexlove._gcConfig.strategy + + if strategy == "disabled" then + return + end + + local currentMemory = collectgarbage("count") / 1024 -- Convert to MB + flexlove._gcState.lastMemory = currentMemory + flexlove._gcState.framesSinceLastGC = flexlove._gcState.framesSinceLastGC + 1 + + -- Check memory threshold (applies to all strategies except disabled) + if currentMemory > flexlove._gcConfig.memoryThreshold then + -- Force full GC when exceeding threshold + collectgarbage("collect") + flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1 + flexlove._gcState.framesSinceLastGC = 0 + return + end + + -- Strategy-specific GC + if strategy == "periodic" then + -- Run incremental GC step every N frames + if flexlove._gcState.framesSinceLastGC >= flexlove._gcConfig.interval then + collectgarbage("step", flexlove._gcConfig.stepSize) + flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1 + flexlove._gcState.framesSinceLastGC = 0 + end + elseif strategy == "auto" then + -- Let Lua's automatic GC handle it, but help with incremental steps + -- Run a small step every frame to keep memory under control + if flexlove._gcState.framesSinceLastGC >= 5 then + collectgarbage("step", 50) -- Small steps to avoid frame drops + flexlove._gcState.framesSinceLastGC = 0 + end + end + -- "manual" strategy: no automatic GC, user must call flexlove.collectGarbage() +end + +--- Manual garbage collection control +---@param mode? string "collect" for full GC, "step" for incremental (default: "collect") +---@param stepSize? number Work units for step mode (default: 200) +function flexlove.collectGarbage(mode, stepSize) + mode = mode or "collect" + stepSize = stepSize or 200 + + if mode == "collect" then + collectgarbage("collect") + flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1 + flexlove._gcState.framesSinceLastGC = 0 + elseif mode == "step" then + collectgarbage("step", stepSize) + elseif mode == "count" then + return collectgarbage("count") / 1024 -- Return memory in MB + end +end + +--- Set GC strategy +---@param strategy string "auto", "periodic", "manual", or "disabled" +function flexlove.setGCStrategy(strategy) + if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then + flexlove._gcConfig.strategy = strategy + else + ErrorHandler.warn("FlexLove", "Invalid GC strategy: " .. tostring(strategy)) + end +end + +--- Get GC statistics +---@return table stats {gcCount, framesSinceLastGC, currentMemoryMB, strategy} +function flexlove.getGCStats() + return { + gcCount = flexlove._gcState.gcCount, + framesSinceLastGC = flexlove._gcState.framesSinceLastGC, + currentMemoryMB = flexlove._gcState.lastMemory, + strategy = flexlove._gcConfig.strategy, + threshold = flexlove._gcConfig.memoryThreshold, + } +end + ---@param text string function flexlove.textinput(text) if flexlove._focusedElement then @@ -562,6 +785,8 @@ end ---@param scancode string ---@param isrepeat boolean function flexlove.keypressed(key, scancode, isrepeat) + -- Handle performance HUD toggle + Performance.keypressed(key) if flexlove._focusedElement then flexlove._focusedElement:keypressed(key, scancode, isrepeat) end @@ -702,6 +927,15 @@ function flexlove.destroy() flexlove.baseScale = nil flexlove.scaleFactors = { x = 1.0, y = 1.0 } flexlove._cachedViewport = { width = 0, height = 0 } + + -- Release canvases explicitly before destroying + if flexlove._gameCanvas then + flexlove._gameCanvas:release() + end + if flexlove._backdropCanvas then + flexlove._backdropCanvas:release() + end + flexlove._gameCanvas = nil flexlove._backdropCanvas = nil flexlove._canvasDimensions = { width = 0, height = 0 } diff --git a/README.md b/README.md index 36b3059..feb9a4d 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,65 @@ onEvent = function(element, event) end ``` +### Deferred Callbacks + +Some LÖVE operations (like `love.window.setMode`) cannot be called while a Canvas is active. FlexLöve provides a deferred callback system to handle these operations safely: + +```lua +-- In your event handler, queue the callback: +onEvent = function(element, event) + if event.type == "click" then + FlexLove.deferCallback(function() + love.window.setMode(1920, 1080, { fullscreen = true }) + end) + end +end + +-- In your love.draw(), execute callbacks after ALL canvases are released: +function love.draw() + love.graphics.setCanvas(myCanvas) + FlexLove.draw() + love.graphics.setCanvas() -- Release ALL canvases + + -- Execute deferred callbacks now that no canvas is active + FlexLove.executeDeferredCallbacks() +end +``` + +**IMPORTANT:** You must call `FlexLove.executeDeferredCallbacks()` at the very end of your `love.draw()` function after releasing all canvases. This ensures callbacks execute in a safe context. + +#### Automatic Deferral with `onEventDeferred` + +Instead of manually wrapping callbacks with `FlexLove.deferCallback()`, you can set the `onEventDeferred` flag to automatically defer all callbacks for that handler: + +```lua +FlexLove.Element.new({ + width = 200, + height = 50, + text = "Change Resolution", + onEvent = function(element, event) + if event.type == "click" then + -- This will be automatically deferred! + love.window.setMode(1920, 1080, { fullscreen = true }) + end + end, + onEventDeferred = true -- Automatically defer all onEvent callbacks +}) +``` + +This flag is available for all event callbacks: +- `onEventDeferred` - Defers `onEvent` callback +- `onFocusDeferred` - Defers `onFocus` callback +- `onBlurDeferred` - Defers `onBlur` callback +- `onTextInputDeferred` - Defers `onTextInput` callback +- `onTextChangeDeferred` - Defers `onTextChange` callback +- `onEnterDeferred` - Defers `onEnter` callback + +Deferred callbacks are useful for: +- Changing window mode/resolution +- Loading resources that modify graphics state +- Any operation that conflicts with active canvas rendering + ### Input Fields FlexLöve provides text input support with single-line (and multi-line coming soon) fields: diff --git a/docs/index.html b/docs/index.html index f052c66..cb1508a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -280,7 +280,7 @@ cp FlexLove/FlexLove.lua your-project/