Preload
Warms registered assets through ContentProvider:PreloadAsync in concurrent batches and publishes progress on the Bus for a loading screen.
- Realm
- Shared
- Require
ReplicatedStorage.ChloeKernel .Preload - Since
- v0.3.0
- Source
- Preload.luau
Preload gathers the assets a game registered, feeds them to ContentProvider:PreloadAsync in batches, and publishes
progress on the Bus so a loading screen can draw it. Up to 4 batches run at once, so one slow asset holds
back only its own batch. Every gathered entry counts exactly once: progress reaches the total and never passes it, and
a failure never aborts the pass. AudioKit and AnimKit banks feed it through
assetIds(). Pool is the other half of paying instance costs up front: it recycles live Instances
instead of warming assets.
Mental model
Section titled “Mental model”Preload.run(kernel, options?) does the first two steps before it returns the handle; the batches run on a spawned
thread:
- It gathers
Assetsfirst, then each bank’sassetIds(). String ids dedupe by exact value, so an id in a bank and inAssetsloads once. Instances pass through untouched and are never deduped.Totalis the gathered count, fixed at this point. Preload.Started(total)publishes synchronously, beforerun()returns.- The list splits into batches of
BatchSize(default 16).Concurrencyworkers (default 4, at least 1) each take the next unclaimed batch until none remain. Each batch is onePreloadAsynccall insidepcall. A batch that throws logs[Preload] batch failed: <error>through Logger and the pass continues. - A listed id that reports
Enum.AssetFetchStatus.TimedOutis held back instead of counted. After every first-pass batch has returned, the held ids run one more pass with the same batching. Whatever that attempt reports is final. Donebecomes true,Preload.Done(loaded, total, failed)publishes, and every thread inawait()resumes.
What counts
Section titled “What counts”Each gathered entry adds exactly one to Loaded, and each count publishes Preload.Progress:
- A listed string id counts when the callback reports it, successful or not, except a first-pass
TimedOut, which waits for the retry. Failures go intoFailed. - An Instance counts once, when its batch returns, with
oktrue if the batch did not throw.PreloadAsyncreports every content id under the Instance through the callback. Those descendant ids do not count towardLoaded, but the ones that fail are added toFailed. - A listed id the callback never reported (its batch threw, or the engine reported it under a different spelling) asks
ContentProvider:GetAssetFetchStatusfor its current status.Successcounts as loaded.TimedOutorLoadingon the first pass joins the retry. Anything else, including a status lookup that errors, counts as failed.
So Loaded equals Total when Preload.Done fires. Before 0.9.0 batches ran one at a time, a thrown batch’s ids never
counted (the bar stopped short), and an Instance counted once per descendant id (the bar overshot).
SurfaceAppearance and MaterialVariant texture packs do not preload; they stream in at runtime.
A controller starts the pass at boot, and the loading screen reads the handle when its ScreenGui binds:
--!strict-- StartupAssets: content the first screen needs before it opens.
return { "rbxassetid://9046898403", "rbxassetid://129423030",}--!strict-- PreloadController: warms the game's startup assets at boot.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Logger = require(ReplicatedStorage.ChloeKernel.Logger)local Preload = require(ReplicatedStorage.ChloeKernel.Preload)local StartupAssets = require(ReplicatedStorage.Game.Config.StartupAssets)
local Log = Logger.scope("PreloadController")
local PreloadController = { Name = "PreloadController",}
function PreloadController.init(self: any, kernel: any) self.Kernel = kernelend
function PreloadController.start(self: any) -- Subscribe before run(): an empty or fully cached pass can finish before run() returns self.Connection = self.Kernel.Bus:subscribe("Preload.Done", function(_topic, _loaded, _total, failed: { string }) if #failed > 0 then Log.warn(`{#failed} assets failed to preload`, table.concat(failed, " ")) end end) self.Warmup = Preload.run(self.Kernel, { Assets = StartupAssets, -- content id strings and Instances -- Banks = { Audio, Anims }: AudioKit or AnimKit instances, or anything with :assetIds() })end
function PreloadController.stop(self: any) if self.Connection then self.Connection:disconnect() endend
return PreloadController--!strict-- Loading screen: fills the bar from Preload progress and closes when warm.
local Loading = {}
function Loading.new(context: any, gui: ScreenGui) local Kernel = context.Kernel local Scope = Kernel:createScope() local Warmup = context.Controllers("PreloadController").Warmup -- nil until that controller has started local Bar = gui:FindFirstChild("Bar", true)
local function show(loaded: number, total: number) if Bar and Bar:IsA("GuiObject") then Bar.Size = UDim2.fromScale(loaded / math.max(total, 1), 1) end end
-- The gui binds when the character first spawns, which can be mid-pass or after it ended: read the handle first local Finished = Warmup ~= nil and Warmup.Done if Warmup then show(Warmup.Loaded, Warmup.Total) end if not Finished then Scope:add(Kernel.Bus:subscribe("Preload.Progress", function(_topic, loaded: number, total: number) show(loaded, total) end)) Scope:add(Kernel.Bus:subscribe("Preload.Done", function() context.close("Loading") end)) end
return { Modal = false, StartsOpen = not Finished, open = function(_self) gui.Enabled = true end, close = function(_self) gui.Enabled = false end, destroy = function(_self) Scope:destroy() end, }end
return LoadingA flow that must block until everything is warm (hold the lobby until the round’s assets load) calls
Warmup:await() from its own thread. The loading screen is also a good moment to run
DeviceBench: Preload keeps the network busy while the bench uses idle CPU.
API reference
Section titled “API reference”| Member | Description |
|---|---|
Preload.run(kernel, options?) -> Handle |
Gathers, publishes Preload.Started, and runs the batches on a spawned thread. Needs kernel.Bus. |
handle:await() |
Yields the calling thread until the pass is done. Returns at once if it already is. |
handle.Done |
True once every batch, including the retry pass, has returned. |
handle.Loaded |
Gathered entries counted so far. Reaches Total at Done. |
handle.Total |
Gathered entry count, fixed when run() gathers. |
handle.Failed |
Array of ids that failed: listed ids and descendant ids of listed Instances. |
Options, all optional:
| Field | Default | Description |
|---|---|---|
Assets |
{} |
Content id strings and Instances. Strings dedupe; Instances pass through. |
Banks |
{} |
Anything with :assetIds() -> { string }: AudioKit, AnimKit or your own registry. |
BatchSize |
16 |
Entries per PreloadAsync call. |
Concurrency |
4 |
Batches in flight at once. Values below 1 count as 1. Added in 0.9.0. |
Preloader |
ContentProvider:PreloadAsync |
Spec seam: (assets, callback(assetId, ok, status?)). |
FetchStatus |
ContentProvider:GetAssetFetchStatus |
Spec seam: (assetId) -> Enum.AssetFetchStatus?. The default is wrapped in pcall and returns nil on error. |
Bus topics
Section titled “Bus topics”| Topic | Payload | Fired |
|---|---|---|
Preload.Started |
(total) |
Once, synchronously inside run(). Subscribe before calling run. |
Preload.Progress |
(loaded, total, assetId, ok, status?) |
Once per gathered entry as it counts. status is the Enum.AssetFetchStatus when there is one; an Instance reports its GetFullName() as assetId and no status. |
Preload.Done |
(loaded, total, failed) |
Once, after the last batch. failed is the handle’s Failed array. |
Design notes
Section titled “Design notes”Totalis fixed whenrun()gathers. Assets registered into a bank afterwards are not picked up, so register banks first and preload last.Preload.Donecan publish beforerun()returns when there is nothing to load or every batch completes without yielding. Subscribe first, or readhandle.Done.