Class UiRuntime
- All Implemented Interfaces:
AutoCloseable
Ui: a single UI thread fed by a
thread-safe (MPSC) task queue, plus a worker pool for the application's
heavy background work.
The owning backend binds the UI thread (bindToCurrentThread()),
drains the queue once per frame (drain()) and sleeps in the native
event wait between frames, never busy-waiting. Posting from any other
thread triggers the UiRuntime.Waker, which the LWJGL backend maps to
glfwPostEmptyEvent() so the sleeping loop wakes up.
Drain semantics: drain() runs only the tasks that were queued
when it started (a snapshot). Tasks posted while draining run on the next
frame, which keeps a self-reposting task from live-locking the loop. The
loop must therefore consult nanosUntilNextDeadline() before
sleeping: 0 means "don't sleep, there is pending work".
Every task is timed; tasks exceeding the slow-task budget (default 8 ms) are logged as warnings, making "my click handler does blocking I/O" contract violations visible during development.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic interfaceWakes the native event loop when work is posted from another thread. -
Constructor Summary
ConstructorsConstructorDescriptionUiRuntime(LongSupplier nanoClock, UiRuntime.Waker waker, ExecutorService workers) Creates a runtime with an injectable clock and worker pool; the pool is not shut down byclose(). -
Method Summary
Modifier and TypeMethodDescription<T> CompletableFuture<T> Runsworkon the worker pool and completes the returned future on the UI thread.voidDeclares the calling thread as the UI thread.voidEnforces thread confinement: throws unless called on the UI thread.voidclose()Shuts down the worker pool if this runtime created it.static UiRuntimecreate(UiRuntime.Waker waker) Creates a runtime with the real clock and an owned daemon worker pool.intdrain()Runs due tasks on the UI thread: first promotes expired delayed tasks, then runs the tasks queued at the moment this call started (snapshot; see class docs).intdrain(), plus a hook for the state a crashed task left behind.booleanbooleanlongHow long the native loop may sleep:0if immediate work is pending,-1if it may sleep indefinitely, otherwise the nanoseconds until the earliest delayed task is due.voidEnqueuesactionto run on the UI thread on the next frame.voidpostDelayed(Runnable action, long delayMillis) Enqueuesactionto run on the UI thread oncedelayMillishave elapsed.voidsetSlowTaskBudgetMillis(long millis) Budget above which a drained task is reported as slow.<T> Work<T> Describes background work with a lifecycle: cancellable, able to report progress, and able to decline delivery when whoever asked has gone away.
-
Constructor Details
-
UiRuntime
Creates a runtime with an injectable clock and worker pool; the pool is not shut down byclose(). Intended for tests.
-
-
Method Details
-
create
Creates a runtime with the real clock and an owned daemon worker pool.- Parameters:
waker- invoked (from arbitrary threads) whenever work is posted from outside the UI thread
-
bindToCurrentThread
public void bindToCurrentThread()Declares the calling thread as the UI thread. Called once by the backend. -
isUiThread
public boolean isUiThread()- Returns:
trueiff called on the bound UI thread
-
checkUiThread
public void checkUiThread()Enforces thread confinement: throws unless called on the UI thread.- Throws:
IllegalStateException- if called from any other thread, or before a UI thread was bound
-
post
Enqueuesactionto run on the UI thread on the next frame. Safe to call from any thread; wakes the native loop when needed.A task that mutates widget or scene state is responsible for invalidating what it touched:
Widget.invalidate(),Widget.markNeedsLayout()orScene.requestRender(). Running a task buys no frame of its own, so a mutation nothing invalidates stays unpainted until something else asks for one. Every widget setter and every tree change invalidates already; a task that writes a field behind a setter's back, or that changes what a customonPaintreads, does not. -
postDelayed
Enqueuesactionto run on the UI thread oncedelayMillishave elapsed. Safe to call from any thread.A task that mutates widget or scene state is responsible for invalidating what it touched: see
post(java.lang.Runnable). Firing buys no frame of its own, which is what makes a timer the cheap way to watch something that rarely changes: a poll that re-reads a value and finds it unchanged costs a wake-up and nothing else, where atickerasks for a frame every frame it stays registered. -
async
Runsworkon the worker pool and completes the returned future on the UI thread. The future's default async executor is the UI thread too, sothenAccept/whenCompletecallbacks land on the UI thread, the canonical "click → fetch → update label" path.Precisely: dependents registered before completion (the normal case, chaining right after this call) and all
*Asyncdependents run on the UI thread. A non-async dependent attached from a background thread after the future already completed runs inline on the attaching thread, andcancel()completes the future on the cancelling thread; in those corners, usethenAcceptAsync(fn)(the default executor is already the UI thread) or attach from the UI thread.A dependent that mutates widget or scene state is responsible for invalidating what it touched: see
post(java.lang.Runnable). Completing on the UI thread buys no frame;label.setText(data)asks for the frame that shows it, a field written directly does not. -
work
Describes background work with a lifecycle: cancellable, able to report progress, and able to decline delivery when whoever asked has gone away. Nothing runs untilWork.start().Reach for this over
async(java.util.function.Supplier<T>)whenever the request can be superseded: a cancelled job delivers nothing, so a view that starts one per keystroke shows the answer to the question last asked, where competingasynccalls show whichever answer happened to finish last. -
uiExecutor
- Returns:
- an
Executorthat posts to the UI thread (post(java.lang.Runnable))
-
workerPool
- Returns:
- the worker pool used by
async(java.util.function.Supplier<T>)
-
drain
public int drain()Runs due tasks on the UI thread: first promotes expired delayed tasks, then runs the tasks queued at the moment this call started (snapshot; see class docs). A task that throws is logged and does not abort the drain, and a task over the slow-task budget is logged as a warning.Draining schedules no repaint. Each task invalidates what it mutated (see
post(java.lang.Runnable)); one that threw did not finish doing so, which is whatdrain(Runnable)exists for.- Returns:
- the number of tasks executed
-
drain
drain(), plus a hook for the state a crashed task left behind.- Parameters:
onTaskCrash- run on the UI thread, inside the drain, once per task that threw, after the crash is logged and reported. A task that throws part-way has applied part of its mutation and invalidated none of it, and how much is unknowable from here, so the caller that owns the surfaces settles them: the LWJGL backend repaints every window. Called on no other path; a task that returns normally is trusted to have invalidated its own work. Runs inside the drain, so it must not throw (an exception from it aborts the remaining tasks).- Returns:
- the number of tasks executed, crashed ones included
-
hasPendingTasks
public boolean hasPendingTasks()- Returns:
trueif immediate tasks are queued right now
-
nanosUntilNextDeadline
public long nanosUntilNextDeadline()How long the native loop may sleep:0if immediate work is pending,-1if it may sleep indefinitely, otherwise the nanoseconds until the earliest delayed task is due. -
setSlowTaskBudgetMillis
public void setSlowTaskBudgetMillis(long millis) Budget above which a drained task is reported as slow. Default: 8 ms. -
close
public void close()Shuts down the worker pool if this runtime created it.- Specified by:
closein interfaceAutoCloseable
-