Help home · Guides · Language reference · Command reference · Parser sample · A* sample

Using Background Jobs

The Blitz3D+ Job system runs finite CPU work and read-only file processing on an isolated background worker. The game or editor stays responsive while the main thread continues to update, render, play audio, and handle input. When the work finishes, the main thread explicitly takes a Bank containing the result.

Extended mode only: Worker Function, StartJob, and the Job commands are available in Extended language mode. They do not change the meaning of existing Original-mode programs.

1. The mental model

Main programBackground worker
Creates a request Bank.Receives an immutable private copy.
Calls StartJob and immediately continues.Runs one compiler-checked Worker Function.
Updates the game, renders, and polls for completion.Performs calculations or reads and processes files.
Takes the result Bank and creates or changes engine objects.Returns one worker-created Bank; it never changes the world directly.

The input and result Banks are messages, not shared memory. This keeps the ordinary Blitz programming model intact: worker code cannot race the main program's worlds, entities, graphics, audio, input, or global variables.

2. Your first Job

A public worker entry accepts one Bank handle and returns one Bank handle. Declare it with Worker Function, then pass its name directly to StartJob:

Worker Function DoubleValue(inputBank)
    outputBank = CreateBank(4)
    PokeInt outputBank, 0, PeekInt(inputBank, 0) * 2
    Return outputBank
End Function

requestBank = CreateBank(4)
PokeInt requestBank, 0, 21
job = StartJob(DoubleValue, requestBank, 1L)
FreeBank requestBank

While Not JobFinished(job)
    ; Keep updating and drawing the game here.
    Delay 1
Wend

If JobState(job) = JOB_SUCCEEDED
    resultBank = TakeJobResult(job)
    Print "Result: " + PeekInt(resultBank, 0)
    FreeBank resultBank
Else
    Print "Job failed: " + JobError(job)
EndIf
FreeJob job

StartJob copies the request before it returns, so immediately changing, resizing, or freeing requestBank is safe. Passing zero instead supplies a valid empty input Bank. A successful result is transferred exactly once by TakeJobResult. The complete runnable version is included with the language reference.

3. What belongs in a Job?

Jobs work best for bounded tasks which can be described by input bytes and produce output bytes:

A Job is not a raw thread and is not intended for a permanent game loop, socket receive loop, file watcher, or one Job per network packet. Keep Jobs finite and reasonably substantial; very tiny tasks usually cost more to submit and transfer than they save.

4. Background asset work

Engine and operating-system handles remain on the main thread. A worker can read and parse a model description, build terrain height data, decode a custom format, or prepare placement records in a Bank. The main thread then uses that Bank to create textures, meshes, terrains, entities, sounds, physics objects, or worlds.

; Main thread
job = StartJob(PrepareLevelData, requestBank, levelRevision)

; Continue showing a menu, loading animation, or the current level.
completed = PollJob()
If completed = job And JobState(job) = JOB_SUCCEEDED
    levelData = TakeJobResult(job)
    ; CreateTerrain, CreateMesh, LoadTexture, and other engine calls go here.
    FreeBank levelData
    FreeJob job
    job = 0
EndIf
Important: the first Job release does not make LoadTexture, LoadMesh, LoadSound, or world commands worker-safe. Use Jobs for the read/parse/prepare stage and publish the finished data on the main thread. Purpose-built asynchronous resource and world loaders can use the same native scheduler in later releases.

5. Polling without stopping the game

PollJob returns one completed Job handle, or zero when there is nothing to collect. It is the usual choice inside a game or editor loop:

Repeat
    completed = PollJob()
    While completed
        Select JobState(completed)
        Case JOB_SUCCEEDED
            resultBank = TakeJobResult(completed)
            ; Validate and apply the result on this main thread.
            FreeBank resultBank
        Case JOB_FAILED
            DebugLog "Background Job failed: " + JobError(completed)
        Case JOB_CANCELLED
            DebugLog "Background Job was cancelled"
        End Select
        FreeJob completed
        completed = PollJob()
    Wend

    ; Update, RenderWorld, draw the interface, and Flip as normal.
Until KeyHit(1)

PollJob consumes only the completion notification. It does not free the Job or take its result. You must still call FreeJob.

JobFinished inspects one known Job without consuming its notification. WaitJob waits for one Job, while WaitAnyJob waits for and consumes any completion. Waiting does not render, call Flip, pump GUI events, or run a generated callback, so games should normally poll instead.

6. Progress and cancellation

A worker can publish progress with SetJobProgress:

For item = 0 To itemCount - 1
    ; Process one item...
    If (item And 63) = 0
        SetJobProgress item, itemCount, "Building navigation data"
    EndIf
Next

On the main thread, JobProgress returns a monotonic value from 0 to 1000. The done, total, and text queries retain the worker's most recent snapshot. Progress is advisory: do not use it as the completion test.

CancelJob requests cooperative cancellation. The compiler inserts cancellation checks at worker entry points and loop backedges. Long indivisible algorithm steps can also test JobCancelled and return early. Cancellation is a request, so continue polling until the Job reaches a terminal state, then free it.

7. Rejecting stale results

Games and editors often start a newer request before an older one finishes. Pass a revision, generation, or request sequence as the Long tag. Before publishing a completion, compare JobTag with the current revision:

buildRevision = buildRevision + 1
job = StartJob(BuildPreview, requestBank, buildRevision)

; Later, after PollJob returns the handle...
If JobTag(job) = buildRevision And JobState(job) = JOB_SUCCEEDED
    previewData = TakeJobResult(job)
    ; Publish the current preview.
    FreeBank previewData
EndIf
FreeJob job ; A stale unclaimed result is discarded here.

Tags do not affect scheduling. They are application-owned values retained unchanged by the Job system.

8. Bank formats and ownership

Treat every request and result Bank as a small binary file format. For data which may evolve, start with a magic value, format version, record count, and validated offsets. Check sizes before reading variable-length records. Prefer integers, Longs, floats, and UTF-8 byte strings with explicit lengths.

ResourceOwner and lifetime
Source request BankThe main program owns it. StartJob copies it before returning.
Worker input BankImmutable and private to the Job; never free or return it.
Worker result BankCreate it inside that Job and return it. Do not free it in the worker.
Taken result BankThe main program owns it and must call FreeBank.
Job handleCall FreeJob after collecting, rejecting, failing, or cancelling it.

Returning zero, returning the input Bank, returning a freed Bank, taking a result twice, or using a Job handle after FreeJob is an error.

9. Worker-safe code

Worker locals may use numeric values, Strings, fixed arrays, local Types, Lists, Maps, Sets, Queues, Stacks, Banks, deterministic math and random commands, and read-only files, directories, and streams. Helper functions called by a worker must also be declared with Worker Function.

The compiler rejects access to main globals, dynamic arrays, ordinary Functions, Methods, constructors with initialisers, engine handles, graphics, worlds, entities, physics, audio, input, GUI, timers, sockets, multiplayer, UserLibs, DLLs, writable files, environment mutation, DATA/READ/Restore, Gosub, nested Jobs, and waits. Runtime checks provide a second boundary. A runtime command is main-only unless it is explicitly marked worker-safe.

10. Failures and cleanup

A worker runtime error fails that Job without unwinding the main program or opening the GUI debugger from the background thread. Inspect JobError, JobErrorFile, JobErrorLine, and JobErrorFunction after JOB_FAILED.

Every submitted Job should finish in one of these ownership paths:

  1. Succeeded and current: take the result, apply it, free the result Bank, then free the Job.
  2. Succeeded but stale: free the Job; its unclaimed result is discarded.
  3. Failed: read or log the diagnostic, then free the Job.
  4. Cancelled: wait or poll for the terminal state, then free the Job.

11. Practical advice

12. Working examples and reference

Back to Guides