Skip to content

Commit

Permalink
load styles for 1.92 previews in a separate thread
Browse files Browse the repository at this point in the history
  • Loading branch information
Bauumm committed Aug 11, 2023
1 parent bbf9d91 commit 8154edc
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 20 deletions.
69 changes: 69 additions & 0 deletions async.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
-- small async implementation using coroutines to suspend a function awaiting a callback
local promise = {}
promise.__index = promise

function promise:new(fn)
local obj = setmetatable({
done_callbacks = {},
error_callbacks = {},
executed = false
}, promise)
fn(function(...)
obj.executed = true
for i = 1, #obj.done_callbacks do
obj.done_callbacks[i](...)
end
end, function(...)
obj.executed = true
for i = 1, #obj.error_callbacks do
obj.error_callbacks[i](...)
end
if #obj.error_callbacks == 0 then
print("Error: ", ...)
error("Uncaught error in promise")
end
end)
return obj
end

function promise:done(callback)
self.done_callbacks[#self.done_callbacks + 1] = callback
return self
end

function promise:err(callback)
self.error_callback[#self.error_callback + 1] = callback
return self
end

local async = setmetatable({}, {
__call = function(_, fn)
return function(...)
local args = {...}
return promise:new(function(resolve, reject)
local co = coroutine.create(function()
local ret = {xpcall(fn, reject, unpack(args))}
if ret[1] then
resolve(unpack(ret, 2))
end
end)
coroutine.resume(co)
end)
end
end
})

async.await = function(prom)
local co = coroutine.running()
if not co then
error("cannot await outide of an async function")
end
prom:done(function(...)
coroutine.resume(co, ...)
end)
return coroutine.yield()
end

async.promise = promise

return async
53 changes: 41 additions & 12 deletions compat/game192/assets.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ local log = require("log")(...)
local args = require("args")
local json = require("extlibs.json.jsonc")
local vfs = require("compat.game192.virtual_filesystem")
local threaded_assets, threadify
if not args.headless then
threadify = require("threadify")
threaded_assets = threadify.require("compat.game192.assets")
end

local assets = {}
local packs = {}
Expand Down Expand Up @@ -128,21 +133,33 @@ function assets.init(data, persistent_data, audio, config)
pack_data.levels[level_json.id] = level_json
end
end
packs[folder] = pack_data
end
end

-- styles have to be loaded here to draw the preview icons in the level selection
pack_data.styles = {}
for contents, filename in
file_ext_read_iter(pack_data.path .. "Styles", ".json", pack_data.virtual_pack_folder.Styles)
do
local style_json
success, style_json = decode_json(contents, filename)
if success then
pack_data.styles[style_json.id] = style_json
end
end
function assets.preload_styles(pack_data)
if pack_data.style_load_promise then
-- already pending
return pack_data.style_load_promise
end
-- load styles in thread for level preview
pack_data.style_load_promise = threaded_assets.load_styles(pack_data):done(function(styles)
pack_data.styles = styles
end)
end

packs[folder] = pack_data
function assets.load_styles(pack_data)
-- styles have to be loaded here to draw the preview icons in the level selection
pack_data.styles = {}
for contents, filename in
file_ext_read_iter(pack_data.path .. "Styles", ".json", pack_data.virtual_pack_folder.Styles)
do
local success, style_json = decode_json(contents, filename)
if success then
pack_data.styles[style_json.id] = style_json
end
end
return pack_data.styles
end

function assets.get_pack_no_load(folder)
Expand Down Expand Up @@ -186,6 +203,18 @@ function assets.get_pack(folder)
end
end

-- styles
if pack_data.style_load_promise and not pack_data.style_load_promise.executed then
-- wait for styles if pending threaded loading not done yet
while not pack_data.style_load_promise.executed do
threadify.update()
love.timer.sleep(0.01)
end
elseif not pack_data.styles then
-- load them synchronously if no threaded loading is pending and styles aren't loaded
assets.load_styles(pack_data)
end

-- events
pack_data.events = {}
for contents, filename in file_ext_read_iter(folder .. "Events", ".json", pack_data.virtual_pack_folder.Events) do
Expand Down
4 changes: 4 additions & 0 deletions compat/game192/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,10 @@ function public.draw_preview(canvas, pack, level)
if not pack_data then
error("pack with id '" .. pack .. "' not found")
end
assets.preload_styles(pack_data)
if pack_data.style_load_promise and not pack_data.style_load_promise.executed then
return pack_data.style_load_promise
end
local level_data = pack_data.levels[level]
if level_data == nil then
error("Level with id '" .. level .. "' not found")
Expand Down
2 changes: 1 addition & 1 deletion game_handler/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ end
---@param level string
function game_handler.draw_preview(canvas, game_version, pack, level)
if games[game_version].draw_preview then
games[game_version].draw_preview(canvas, pack, level)
return games[game_version].draw_preview(canvas, pack, level)
end
end

Expand Down
5 changes: 5 additions & 0 deletions main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ function love.run()
end

local game_handler = require("game_handler")
local threadify = require("threadify")
global_config.init(config, game_handler.profile)
game_handler.init(config)
local ui = require("ui")
Expand All @@ -189,12 +190,16 @@ function love.run()
love.event.pump()
for name, a, b, c, d, e, f in love.event.poll() do
if name == "quit" then
threadify.stop()
return a or 0
elseif name == "threaderror" then
error("Error in thread: " .. b)
end
game_handler.process_event(name, a, b, c, d, e, f)
ui.process_event(name, a, b, c, d, e, f)
end

threadify.update()
ui.update(love.timer.getDelta())

-- ensures tickrate on its own
Expand Down
96 changes: 96 additions & 0 deletions threadify.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
local log = require("log")("threadify")
local modname, is_thread = ...

if is_thread then
local api = require(modname)
local run = true
while run do
local cmd = love.thread.getChannel(modname .. "_cmd"):demand()
local call_id = cmd[1]
local out_channel = love.thread.getChannel(modname .. "_out")
xpcall(function()
local fn = api[cmd[2]]
out_channel:push({ call_id, true, fn(unpack(cmd, 3)) })
end, function(err)
out_channel:push({ call_id, false, "Failed to call '" .. modname .. "." .. cmd[2] .. "'", err })
end)
end
else
local async = require("async")
local threads = {}
local thread_names = {}
local threadify = {}

function threadify.require(require_string)
if not threads[require_string] then
local thread_table = {
thread = love.thread.newThread("threadify.lua"),
resolvers = {},
rejecters = {},
free_indices = {},
}
thread_table.thread:start(require_string, true)
threads[require_string] = thread_table
thread_names[#thread_names + 1] = require_string
end
local thread = threads[require_string]
local interface = {}
return setmetatable(interface, {
__index = function(_, key)
return function(...)
local msg = {-1, key, ...}
return async.promise:new(function(resolve, reject)
local index = 0
if #thread.free_indices == 0 then
index = #thread.resolvers + 1
else
local last_index = #thread.free_indices
index = thread.free_indices[last_index]
thread.free_indices[last_index] = nil
end
msg[1] = index
love.thread.getChannel(require_string .. "_cmd"):push(msg)
thread.resolvers[index] = resolve
thread.rejecters[index] = reject
end)
end
end
})
end

function threadify.update()
for i = 1, #thread_names do
local require_string = thread_names[i]
local thread = threads[require_string]
local result = love.thread.getChannel(require_string .. "_out"):pop()
if result then
if result[2] then
thread.resolvers[result[1]](unpack(result, 3))
else
log(result[3])
thread.rejecters[result[1]](result[4])
end
thread.resolvers[result[1]] = nil
thread.rejecters[result[1]] = nil
thread.free_indices[#thread.free_indices + 1] = result[1]
end
end
end

function threadify.stop()
for require_string, thread_table in pairs(threads) do
local thread = thread_table.thread
if thread:isRunning() then
-- effectively kills the thread (sending stop doesn't work sometimes and when it does it would still cause unresponsiveness on closing)
thread:release()
else
local err = thread:getError()
if err then
log("Error in '" .. require_string .. "' thread: " .. err)
end
end
end
end

return threadify
end
46 changes: 39 additions & 7 deletions ui/elements/level_preview.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local element = require("ui.elements.element")
local game_handler = require("game_handler")
local signal = require("ui.anim.signal")
local level_preview = {}
level_preview.__index = setmetatable(level_preview, {
__index = element,
Expand All @@ -20,11 +21,20 @@ local function redraw(self)
love.graphics.setCanvas(canvas)
love.graphics.origin()
love.graphics.clear(0, 0, 0, 1)
love.graphics.translate(canvas:getWidth() / 2, canvas:getHeight() / 2)
local half_size = canvas:getWidth() / 2
love.graphics.translate(half_size, half_size)
love.graphics.scale(GAME_SCALE, GAME_SCALE)
love.graphics.setColor(1, 1, 1, 1)
game_handler.draw_preview(canvas, self.game_version, self.pack, self.level)
local promise = game_handler.draw_preview(canvas, self.game_version, self.pack, self.level)
love.graphics.setCanvas()
if promise then
-- styles not loaded yet
promise:done(function()
-- redraw once styles are loaded
redraw(self)
end)
return
end
self.image = love.graphics.newImage(canvas:newImageData())
end

Expand All @@ -35,6 +45,9 @@ function level_preview:new(game_version, pack, level, options)
pack = pack,
level = level,
last_scale = 1,
angle = signal.new_waveform(1, function(x)
return x * 2 * math.pi
end),
}, level_preview),
options
)
Expand All @@ -54,11 +67,30 @@ function level_preview:draw()
redraw(self)
end
self.last_scale = self.scale
love.graphics.draw(
self.image,
self.bounds[1] + self.padding * self.scale,
self.bounds[2] + self.padding * self.scale
)
local pos_x, pos_y = self.bounds[1] + self.padding * self.scale, self.bounds[2] + self.padding * self.scale
if self.image then
love.graphics.draw(
self.image,
self.bounds[1] + self.padding * self.scale,
self.bounds[2] + self.padding * self.scale
)
else
-- loading circle (TODO: replace hardcoded colors and line width maybe)
local half_size = SIZE * self.scale / 2
local center_x, center_y = pos_x + half_size, pos_y + half_size
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setLineWidth(self.scale * 5)
love.graphics.circle("line", center_x, center_y, half_size / 2, 100)
love.graphics.setColor(0, 0, 0, 1)
local half_sector_size = math.pi / 4
local radius = half_size
love.graphics.polygon(
"fill",
center_x, center_y,
center_x + math.cos(self.angle() - half_sector_size) * radius, center_y + math.sin(self.angle() - half_sector_size) * radius,
center_x + math.cos(self.angle() + half_sector_size) * radius, center_y + math.sin(self.angle() + half_sector_size) * radius
)
end
end

return level_preview

0 comments on commit 8154edc

Please sign in to comment.