caching perf improvements - major improvements for immediate mode
This commit is contained in:
41
FlexLove.lua
41
FlexLove.lua
@@ -377,36 +377,37 @@ function flexlove.endFrame()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Save state back for all elements created this frame
|
-- Save state back for all elements created this frame (with diffing optimization)
|
||||||
for _, element in ipairs(flexlove._currentFrameElements) do
|
for _, element in ipairs(flexlove._currentFrameElements) do
|
||||||
if element.id and element.id ~= "" then
|
if element.id and element.id ~= "" then
|
||||||
local state = StateManager.getState(element.id, {})
|
-- Build state update object
|
||||||
|
local stateUpdate = {}
|
||||||
|
|
||||||
-- Save stateful properties back to persistent state
|
|
||||||
-- Get event handler state
|
-- Get event handler state
|
||||||
if element._eventHandler then
|
if element._eventHandler then
|
||||||
local eventState = element._eventHandler:getState()
|
local eventState = element._eventHandler:getState()
|
||||||
for k, v in pairs(eventState) do
|
for k, v in pairs(eventState) do
|
||||||
state[k] = v
|
stateUpdate[k] = v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
state._focused = element._focused
|
|
||||||
state._focused = element._focused
|
stateUpdate._focused = element._focused
|
||||||
state._cursorPosition = element._cursorPosition
|
stateUpdate._cursorPosition = element._cursorPosition
|
||||||
state._selectionStart = element._selectionStart
|
stateUpdate._selectionStart = element._selectionStart
|
||||||
state._selectionEnd = element._selectionEnd
|
stateUpdate._selectionEnd = element._selectionEnd
|
||||||
state._textBuffer = element._textBuffer
|
stateUpdate._textBuffer = element._textBuffer
|
||||||
state._scrollX = element._scrollX
|
stateUpdate._scrollX = element._scrollX
|
||||||
state._scrollY = element._scrollY
|
stateUpdate._scrollY = element._scrollY
|
||||||
state._scrollbarDragging = element._scrollbarDragging
|
stateUpdate._scrollbarDragging = element._scrollbarDragging
|
||||||
state._hoveredScrollbar = element._hoveredScrollbar
|
stateUpdate._hoveredScrollbar = element._hoveredScrollbar
|
||||||
state._scrollbarDragOffset = element._scrollbarDragOffset
|
stateUpdate._scrollbarDragOffset = element._scrollbarDragOffset
|
||||||
state._cursorBlinkTimer = element._cursorBlinkTimer
|
stateUpdate._cursorBlinkTimer = element._cursorBlinkTimer
|
||||||
state._cursorVisible = element._cursorVisible
|
stateUpdate._cursorVisible = element._cursorVisible
|
||||||
state._cursorBlinkPaused = element._cursorBlinkPaused
|
stateUpdate._cursorBlinkPaused = element._cursorBlinkPaused
|
||||||
state._cursorBlinkPauseTimer = element._cursorBlinkPauseTimer
|
stateUpdate._cursorBlinkPauseTimer = element._cursorBlinkPauseTimer
|
||||||
|
|
||||||
StateManager.setState(element.id, state)
|
-- Use optimized update that only changes modified values
|
||||||
|
StateManager.updateStateIfChanged(element.id, stateUpdate)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,36 @@ local unpack = table.unpack or unpack
|
|||||||
local Cache = {
|
local Cache = {
|
||||||
canvases = {},
|
canvases = {},
|
||||||
quads = {},
|
quads = {},
|
||||||
|
blurInstances = {}, -- Cache blur instances by quality
|
||||||
MAX_CANVAS_SIZE = 20,
|
MAX_CANVAS_SIZE = 20,
|
||||||
MAX_QUAD_SIZE = 20,
|
MAX_QUAD_SIZE = 20,
|
||||||
|
INTENSITY_THRESHOLD = 5, -- Skip blur below this intensity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
--- Round canvas size to nearest bucket for better reuse
|
||||||
|
---@param size number Size to bucket
|
||||||
|
---@return number bucketSize Bucketed size
|
||||||
|
local function bucketSize(size)
|
||||||
|
if size <= 128 then
|
||||||
|
return math.ceil(size / 32) * 32
|
||||||
|
elseif size <= 512 then
|
||||||
|
return math.ceil(size / 64) * 64
|
||||||
|
elseif size <= 1024 then
|
||||||
|
return math.ceil(size / 128) * 128
|
||||||
|
else
|
||||||
|
return math.ceil(size / 256) * 256
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Get or create a canvas from cache
|
--- Get or create a canvas from cache
|
||||||
---@param width number Canvas width
|
---@param width number Canvas width
|
||||||
---@param height number Canvas height
|
---@param height number Canvas height
|
||||||
---@return love.Canvas canvas The cached or new canvas
|
---@return love.Canvas canvas The cached or new canvas
|
||||||
function Cache.getCanvas(width, height)
|
function Cache.getCanvas(width, height)
|
||||||
local key = string.format("%dx%d", width, height)
|
-- Use bucketed sizes for better cache reuse
|
||||||
|
local bucketedWidth = bucketSize(width)
|
||||||
|
local bucketedHeight = bucketSize(height)
|
||||||
|
local key = string.format("%dx%d", bucketedWidth, bucketedHeight)
|
||||||
|
|
||||||
if not Cache.canvases[key] then
|
if not Cache.canvases[key] then
|
||||||
Cache.canvases[key] = {}
|
Cache.canvases[key] = {}
|
||||||
@@ -28,11 +48,14 @@ function Cache.getCanvas(width, height)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local canvas = love.graphics.newCanvas(width, height)
|
local canvas = love.graphics.newCanvas(bucketedWidth, bucketedHeight)
|
||||||
table.insert(cache, { canvas = canvas, inUse = true })
|
table.insert(cache, { canvas = canvas, inUse = true })
|
||||||
|
|
||||||
if #cache > Cache.MAX_CANVAS_SIZE then
|
if #cache > Cache.MAX_CANVAS_SIZE then
|
||||||
table.remove(cache, 1)
|
local removed = table.remove(cache, 1)
|
||||||
|
if removed and removed.canvas then
|
||||||
|
removed.canvas:release()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return canvas
|
return canvas
|
||||||
@@ -102,6 +125,7 @@ end
|
|||||||
function Cache.clear()
|
function Cache.clear()
|
||||||
Cache.canvases = {}
|
Cache.canvases = {}
|
||||||
Cache.quads = {}
|
Cache.quads = {}
|
||||||
|
Cache.blurInstances = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
@@ -169,6 +193,27 @@ function ShaderBuilder.build(taps, offset, offsetType, sigma)
|
|||||||
return love.graphics.newShader(shaderCode)
|
return love.graphics.newShader(shaderCode)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get or create a blur instance from cache
|
||||||
|
---@param quality number Quality level (1-10)
|
||||||
|
---@return table blurData Cached blur data {shader, taps}
|
||||||
|
function Cache.getBlurInstance(quality)
|
||||||
|
if not Cache.blurInstances[quality] then
|
||||||
|
local taps = 3 + (quality - 1) * 1.5
|
||||||
|
taps = math.floor(taps)
|
||||||
|
if taps % 2 == 0 then
|
||||||
|
taps = taps + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local shader = ShaderBuilder.build(taps, 1.0, "weighted", -1)
|
||||||
|
Cache.blurInstances[quality] = {
|
||||||
|
shader = shader,
|
||||||
|
taps = taps,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return Cache.blurInstances[quality]
|
||||||
|
end
|
||||||
|
|
||||||
---@class BlurProps
|
---@class BlurProps
|
||||||
---@field quality number? Quality level (1-10, default: 5)
|
---@field quality number? Quality level (1-10, default: 5)
|
||||||
|
|
||||||
@@ -189,26 +234,13 @@ function Blur.new(props)
|
|||||||
local quality = props.quality or 5
|
local quality = props.quality or 5
|
||||||
quality = math.max(1, math.min(10, quality))
|
quality = math.max(1, math.min(10, quality))
|
||||||
|
|
||||||
-- Map quality to shader parameters
|
-- Get cached blur instance for this quality level
|
||||||
-- Quality 1: 3 taps (fastest, lowest quality)
|
local blurData = Cache.getBlurInstance(quality)
|
||||||
-- Quality 5: 7 taps (balanced)
|
|
||||||
-- Quality 10: 15 taps (slowest, highest quality)
|
|
||||||
local taps = 3 + (quality - 1) * 1.5
|
|
||||||
taps = math.floor(taps)
|
|
||||||
if taps % 2 == 0 then
|
|
||||||
taps = taps + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local offset = 1.0
|
|
||||||
local offsetType = "weighted"
|
|
||||||
local sigma = -1
|
|
||||||
|
|
||||||
local shader = ShaderBuilder.build(taps, offset, offsetType, sigma)
|
|
||||||
|
|
||||||
local self = setmetatable({}, Blur)
|
local self = setmetatable({}, Blur)
|
||||||
self.shader = shader
|
self.shader = blurData.shader
|
||||||
self.quality = quality
|
self.quality = quality
|
||||||
self.taps = taps
|
self.taps = blurData.taps
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@@ -233,6 +265,12 @@ function Blur:applyToRegion(intensity, x, y, width, height, drawFunc)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Early exit for very low intensity (optimization)
|
||||||
|
if intensity < Cache.INTENSITY_THRESHOLD then
|
||||||
|
drawFunc()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
intensity = math.max(0, math.min(100, intensity))
|
intensity = math.max(0, math.min(100, intensity))
|
||||||
|
|
||||||
-- Intensity 0-100 maps to 0-5 passes
|
-- Intensity 0-100 maps to 0-5 passes
|
||||||
@@ -302,6 +340,11 @@ function Blur:applyBackdrop(intensity, x, y, width, height, backdropCanvas)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Early exit for very low intensity (optimization)
|
||||||
|
if intensity < Cache.INTENSITY_THRESHOLD then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
intensity = math.max(0, math.min(100, intensity))
|
intensity = math.max(0, math.min(100, intensity))
|
||||||
|
|
||||||
local passes = math.ceil(intensity / 20)
|
local passes = math.ceil(intensity / 20)
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ function LayoutEngine.new(props, deps)
|
|||||||
self._layoutCount = 0
|
self._layoutCount = 0
|
||||||
self._lastFrameCount = 0
|
self._lastFrameCount = 0
|
||||||
|
|
||||||
|
-- Layout memoization cache
|
||||||
|
self._layoutCache = {
|
||||||
|
childrenCount = 0,
|
||||||
|
containerWidth = 0,
|
||||||
|
containerHeight = 0,
|
||||||
|
childrenHash = "",
|
||||||
|
}
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -169,6 +177,14 @@ function LayoutEngine:layoutChildren()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Check if layout can be skipped (memoization optimization)
|
||||||
|
if self:_canSkipLayout() then
|
||||||
|
if timerName and LayoutEngine._Performance then
|
||||||
|
LayoutEngine._Performance:stopTimer(timerName)
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
-- Track layout recalculations for performance warnings
|
-- Track layout recalculations for performance warnings
|
||||||
self:_trackLayoutRecalculation()
|
self:_trackLayoutRecalculation()
|
||||||
|
|
||||||
@@ -987,6 +1003,44 @@ function LayoutEngine:recalculateUnits(newViewportWidth, newViewportHeight)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Check if layout can be skipped based on cached state (memoization)
|
||||||
|
---@return boolean canSkip True if layout hasn't changed and can be skipped
|
||||||
|
function LayoutEngine:_canSkipLayout()
|
||||||
|
if not self.element then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local childrenCount = #self.element.children
|
||||||
|
local containerWidth = self.element.width
|
||||||
|
local containerHeight = self.element.height
|
||||||
|
|
||||||
|
-- Generate simple hash of children dimensions
|
||||||
|
local childrenHash = ""
|
||||||
|
for i, child in ipairs(self.element.children) do
|
||||||
|
if i <= 5 then -- Only hash first 5 children for performance
|
||||||
|
childrenHash = childrenHash .. child.width .. "x" .. child.height .. ","
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local cache = self._layoutCache
|
||||||
|
|
||||||
|
-- Check if layout inputs have changed
|
||||||
|
if cache.childrenCount == childrenCount and
|
||||||
|
cache.containerWidth == containerWidth and
|
||||||
|
cache.containerHeight == containerHeight and
|
||||||
|
cache.childrenHash == childrenHash then
|
||||||
|
return true -- Layout hasn't changed, can skip
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update cache with current values
|
||||||
|
cache.childrenCount = childrenCount
|
||||||
|
cache.containerWidth = containerWidth
|
||||||
|
cache.containerHeight = containerHeight
|
||||||
|
cache.childrenHash = childrenHash
|
||||||
|
|
||||||
|
return false -- Layout has changed, must recalculate
|
||||||
|
end
|
||||||
|
|
||||||
--- Track layout recalculations and warn about excessive layouts
|
--- Track layout recalculations and warn about excessive layouts
|
||||||
function LayoutEngine:_trackLayoutRecalculation()
|
function LayoutEngine:_trackLayoutRecalculation()
|
||||||
if not LayoutEngine._Performance or not LayoutEngine._Performance.warningsEnabled then
|
if not LayoutEngine._Performance or not LayoutEngine._Performance.warningsEnabled then
|
||||||
|
|||||||
@@ -340,13 +340,42 @@ end
|
|||||||
function StateManager.updateState(id, newState)
|
function StateManager.updateState(id, newState)
|
||||||
local state = StateManager.getState(id)
|
local state = StateManager.getState(id)
|
||||||
|
|
||||||
-- Merge new state into existing state
|
-- Merge new state into existing state (with diffing optimization)
|
||||||
|
local changed = false
|
||||||
for key, value in pairs(newState) do
|
for key, value in pairs(newState) do
|
||||||
state[key] = value
|
if state[key] ~= value then
|
||||||
|
state[key] = value
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Update metadata
|
-- Only update metadata if something actually changed
|
||||||
stateMetadata[id].lastFrame = frameNumber
|
if changed then
|
||||||
|
stateMetadata[id].lastFrame = frameNumber
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Update state only if values have changed (optimized for immediate mode)
|
||||||
|
---@param id string Element ID
|
||||||
|
---@param newState table New state values to merge
|
||||||
|
---@return boolean changed True if any values changed
|
||||||
|
function StateManager.updateStateIfChanged(id, newState)
|
||||||
|
local state = StateManager.getState(id)
|
||||||
|
local changed = false
|
||||||
|
|
||||||
|
for key, value in pairs(newState) do
|
||||||
|
-- Skip if value hasn't changed (optimization)
|
||||||
|
if state[key] ~= value then
|
||||||
|
state[key] = value
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
stateMetadata[id].lastFrame = frameNumber
|
||||||
|
end
|
||||||
|
|
||||||
|
return changed
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Clear state for a specific element ID
|
--- Clear state for a specific element ID
|
||||||
|
|||||||
@@ -193,8 +193,17 @@ end
|
|||||||
---@param fontPath string?
|
---@param fontPath string?
|
||||||
---@return love.Font
|
---@return love.Font
|
||||||
function FONT_CACHE.get(size, fontPath)
|
function FONT_CACHE.get(size, fontPath)
|
||||||
-- Round size to reduce cache entries (e.g., 14.5 -> 15, 14.7 -> 15)
|
-- Bucket font sizes for better cache reuse (reduces unique cache entries)
|
||||||
size = math.floor(size + 0.5)
|
-- Small sizes (< 20): round to nearest 2
|
||||||
|
-- Medium sizes (20-40): round to nearest 4
|
||||||
|
-- Large sizes (> 40): round to nearest 8
|
||||||
|
if size < 20 then
|
||||||
|
size = math.floor((size + 1) / 2) * 2
|
||||||
|
elseif size < 40 then
|
||||||
|
size = math.floor((size + 2) / 4) * 4
|
||||||
|
else
|
||||||
|
size = math.floor((size + 4) / 8) * 8
|
||||||
|
end
|
||||||
|
|
||||||
local cacheKey = fontPath and (fontPath .. ":" .. tostring(size)) or ("default:" .. tostring(size))
|
local cacheKey = fontPath and (fontPath .. ":" .. tostring(size)) or ("default:" .. tostring(size))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user