Architecture
The shape of the engine at build level: the targets, the tree, what a module is, what a game project is, and where the engine/game boundary runs. PHILOSOPHY.md holds the why behind every choice here; this document is the where things live, and it is the authoritative roster of modules and folders. Like the philosophies, it describes the destination, not the current codebase.
The targets
Section titled “The targets”Product libraries, sample applications, benchmark executables and tests; one dependency direction and one shared compiler-settings target (see the target table in PHILOSOPHY.md). An arrow reads “links against”; no arrow ever points the other way — an engine file including a game header fails the build, and that is the feature (T5).
It fails on the compiler and on a check, and the difference between
the two is worth stating plainly rather than leaving to be discovered.
Includes are written from the repository root, so an engine file says
#include "engine/render/renderer.h" — which means the engine’s own
include root has to be the directory above engine/. That directory
used to hold game/ too, and no include path admits the first spelling
and refuses #include "game/objects/level.h" while the two are
siblings; cmake/check_engine_includes.cmake was the whole of the wall
for exactly as long as that was true. The repo split ended it. The
paint-shooter is in its own repository consuming this one as a submodule,
nothing named game/ sits beside engine/ any more, and the offending
include now fails like any other missing header:
engine\scene\scene.cpp(1): fatal error C1083: Cannot open include file:'game/objects/level.h': No such file or directoryThe check stays anyway, and not out of sentiment. The compiler only
enforces the rule when this repository is built standalone. A client
consuming the engine as a subdirectory builds it from a tree that very
likely does have a game/ in it, and that client’s include roots are not
the engine’s business — there, the grep is the only thing holding the
line. It runs on every build and fails with the offending file and line.
flowchart TD
subgraph apps["applications"]
game["ColourWarsGame<br/>the paint-shooter"]
sample["MinimalSample<br/>new-project template"]
linesweeper["LineSweeperSample<br/>whole sample game"]
collision_sample["CollisionSample"]
guides["AudioSample<br/>LocalMultiplayerSample"]
throughput["LabradorBench<br/>complexity gates"]
frame["LineSweeperFrameBench<br/>declared hardware measurement"]
capture["LineSweeperCapture<br/>exact images of the sample"]
tests["tests"]
end
subgraph libs["static libraries"]
engine["LabradorEngine"]
math["MattMath"]
rules["LineSweeperRules"]
collision_example["CollisionExample"]
end
subgraph edge["the bought edge (T9)"]
sdks["platform SDKs<br/>graphics · DirectXTK · XInput"]
rapidjson["rapidjson"]
end
game --> engine
sample --> engine
linesweeper --> engine
linesweeper --> rules
collision_sample --> collision_example
collision_example --> engine
guides --> engine
throughput --> engine
frame --> engine
frame --> rules
capture --> engine
capture --> rules
tests --> engine
tests --> math
tests --> rules
engine --> math
engine --> sdks
engine --> rapidjson
The sample game is a target from the day the split lands: it is the permanent second client that keeps the boundary honest (T1), and the template a new project copies.
The build
Section titled “The build”CMake, plain and boring (T4). No IDE owns the project: an engine offered
to strangers cannot require their editor, and the eventual second
platform (Targets and layout, PHILOSOPHY.md) cannot be reached from a
.sln.
- The root
CMakeLists.txtdeclares the targets and nothing else; each target directory owns its own. Nothing IDE-specific is committed — a solution file, for whoever wants one, is generated output. - The shared-settings job is an
INTERFACEtarget every real target links: the language standard,/W4, warnings-as-errors, in exactly one place — so a game is compiled as strictly as the engine that hosts it (T5). - The bought edge (T9) is declared, not clicked:
vcpkg.json(manifest mode) names DirectXTK and the XAudio2 redistributable; header-only rapidjson stays vendored inexternal/. - Tests use a portable test framework and register with CTest:
ctestruns everything, on a contributor’s machine or in CI, with no IDE present. CI is.github/workflows/ci.yml- debug and release, configure, build and test. Headless audio checks use the null backend; the audio sample’s checked-in wave bank also permits a separate playback check on a machine with an audio endpoint. - Benchmarks register with CTest too, and assert on complexity class rather than on wall-clock: a phase that is linear in the object count must stay linear when the count quadruples, whatever the machine. An absolute threshold would either fail on a slow box or pass on a fast one after a real regression.
- Builds are out-of-source (
out/, ignored); the source tree never contains build products.
Visual Studio remains a first-class way to work — it opens a CMake folder natively, with debugging and IntelliSense intact. It just stops being load-bearing.
The tree
Section titled “The tree”/├── CMakeLists.txt the root build file: lists the targets, nothing else├── CMakePresets.json the configurations: debug and release for each│ of the five render backends — ten in all├── vcpkg.json the bought edge, declared (T9)├── cmake/ the shared settings target, helper modules├── engine/ the product│ ├── math/ MattMath — depends on nothing, and links│ │ nothing: no DirectXTK, no D3D11, no Windows│ ├── core/ game loop, fixed-step timing, states, services,│ │ registries, reading numbers out of a file│ ├── render/ the Renderer, cameras, viewports, colours, fonts,│ │ │ the file readers and the quad arithmetic — every│ │ │ decision that shows on screen│ │ ├── sprite.hlsl the only shader, compiled at build time by fxc for│ │ │ the two Direct3D backends and by dxc for Vulkan —│ │ │ three backends, two compilers, a profile each│ │ ├── d3d11/ the D3D11 backend: a device, buffers, five states│ │ ├── d3d12/ the D3D12 backend: a device, a queue, a fence and│ │ │ frames in flight — the one API where the engine│ │ │ owns synchronisation│ │ ├── gl/ the OpenGL 3.3 core backend: a WGL context, a│ │ │ loader for the thirty-six entry points it uses,│ │ │ and the same shader in GLSL│ │ ├── vulkan/ the Vulkan backend: the one API that reaches│ │ │ other platforms, and the one where the│ │ │ presentation engine rather than the window says│ │ │ the size changed│ │ └── null/ no graphics API at all: it records what it was│ │ asked to draw, so a test can assert drawing on a│ │ machine with no driver│ ├── collision/ contacts, the broad phase that prunes the pairs,│ │ the narrow phase, manifolds, resolution│ ├── scene/ Scene: the object list, the view list, the tick│ │ phases, the per-view draw fan-out and the cull│ ├── input/ devices: polling, deadzones, press edges, typed│ │ text, and the direction a stick or a d-pad is│ │ pushed. Three devices, one API shape, two ways│ │ in — the pads are read from a backend, the│ │ keyboard and mouse are fed from the window (see│ │ below). The action map is deliberately not built│ │ - neither client has a rebinding screen, so a│ │ binding table would be a speculative framework│ │ (T1). A Direction is not one: it names what a│ │ device did, not what the game calls it│ │ └── xinput/ the XInput backend. There is no win32/ beside│ │ it: the keyboard and mouse have no backend to│ │ select, because their platform edge is the│ │ window and it already exists│ ├── audio/ playback, mixing. AudioDevice is the seam and it│ │ │ is the whole of what an audio API does - open a│ │ │ container, find a name in it, build a voice, and│ │ │ start, stop, adjust or report one. Which wave a│ │ │ name means, which handles are valid and where a│ │ │ level is clamped are above it│ │ ├── xaudio2/ the XAudio2 backend, through DirectXTK, and the│ │ │ only file in the tree that includes <Audio.h>. It│ │ │ owns the container: a wave bank is an .xwb here│ │ └── null/ no audio API at all: it records what it was asked│ │ to play, so a test can assert playing on a machine│ │ with no sound card│ ├── ui/ widgets, focus, controller navigation│ ├── assets/ JSON loading, resource loaders, manifest, factories│ └── app/ the application shell: window, device, services,│ main loop, state stack├── samples/ a starter, a whole game and focused guide examples│ ├── minimal/ the new-project template: the smallest thing│ that runs, and the one you copy. The│ paint-shooter that used to sit beside it under│ game/ is its own repository now, and consumes│ this one as a submodule│ ├── collision/ layers, wall resolution and a trigger│ ├── audio/ original playable tones and a sound bank│ ├── local_multiplayer/ two players and two views of one world│ └── linesweeper/ a whole game, and the one you read. Three│ ├── rules/ layers: rules/ links nothing, so the game is│ ├── presentation/ playable from tests/ with no device;│ └── states/ presentation/ reads a world and draws it; and│ states/ is the only place that includes both│ rules/ and engine/. Its own README records the│ decisions behind it├── tests/ module tests and sample behavior tests; collision│ ├── app/ and audio examples keep theirs beside the sample│ ├── assets/│ ├── audio/│ ├── collision/│ ├── core/│ ├── input/│ ├── linesweeper/ the falling-block game and its particle field,│ │ in two targets: one links LineSweeperRules and│ │ no engine, the other links the engine and still│ │ needs no device│ ├── local_multiplayer/ input ownership, cameras and per-view drawing│ ├── math/│ ├── render/│ ├── scene/│ └── ui/├── bench/ complexity gates beside ctest, plus the explicit│ hardware frame-cadence executable outside it├── tools/ development operations outside the product targets│ ├── cloud_performance/ one frozen, one-shot EC2 reference measurement│ └── linesweeper_capture/ exact, repeatable images of the sample, for│ the website: a scripted match through the rules,│ drawn on a device and read back├── external/ third-party source: rapidjson. DirectXTK is not│ here — it is a vcpkg package (vcpkg.json)├── website/ the public website, built with npm and never by│ CMake. It reads docs/design/, the samples and the│ engine headers from this same checkout, so what it│ publishes is the revision it was built from├── .github/workflows/ CI: the engine, and the website beside it└── docs/ ├── design/ philosophies, conventions, this document ├── file-length-audit.md which files are long, and which of them earn it ├── performance/ what a declared host measured: one dated file per │ complete run of the cloud lane, raw evidence cited, │ and one per finding those runs settle between them ├── port/ what a second platform costs, and in what order ├── repo-split.md the verified procedure for splitting engine from game ├── review/ findings against the current code ├── survey/ a sweep of the whole tree asking what to build next. │ One per sweep, dated, in pairs: <date>.md is the │ survey as written and is not updated as items land, │ <date>-status.md is what landed and what it returned ├── website-plan.md what the website is for, and what is decided about it └── website-inventory.md what its documentation can be built from, by moduleEvery target directory owns its own CMakeLists.txt; the root file only
lists them. Every file picks its home the day it is created.
Platform-specific code
lives only in the backend subfolders (render/d3d11/, audio/xaudio2/,
input/xinput/), behind engine-owned interfaces, so a second platform is
an addition, not a rewrite. There is a third case and it is named here rather than left to
be discovered: the shell’s window, app/window.{h,cpp}. app is already
the one module allowed to depend on everything, PHILOSOPHY lists
windowing alongside the rendering backend as platform code at the edge,
and the seam is the same shape as the others — Window translates
messages and WindowNotify is what the owner implements, so nothing
above it names a Win32 type. It is not in app/win32/ because there is
no second platform to select between and inventing the folder now is the
speculative framework T1 rules out; when one arrives the pair moves down
a folder without renaming the class or touching a call site.
That third case now carries two of the three input devices, and the
asymmetry is worth stating because it looks like an inconsistency and is
not. A gamepad is read: input/xinput/ asks XInput for a complete
snapshot whenever Gamepads::poll wants one, and owes the window
nothing. A keyboard and a mouse are fed: they reach a Win32 program
only as messages in a window’s queue, and one of their channels — typed
text — cannot be rebuilt from device state at any price, because the
shift resolution, the key repeat, the dead keys and the IME have all
happened inside the OS before the character exists. The wheel is the same
shape for a different reason: it has no position to sample, only deltas
that are gone if nobody was listening.
So the flow for those two is app → input, never the reverse. Window
translates the messages into Key and MouseButton — the engine’s own
names, so nothing above window.cpp meets a VK_ constant or a UTF-16
code unit — WindowNotify carries them out, Application forwards them
into the devices, and the devices latch them into frames. The module
table is untouched by all of it: input still depends on core and
math alone, and nothing in it knows a window exists.
What a game sees is the same shape for all three devices — state,
previous_state, held, pressed, released, and an edge that
requires the device to have been live on both frames. That last rule is
gamepad.h’s, transplanted: a pad that was unplugged and a window that
lost the foreground produce the identical phantom press, and a client
should not have to learn two spellings to suppress one bug.
Those interfaces are concrete classes with one implementation selected
at build time, not abstract bases with vtables. Renderer is declared
once in engine/render/renderer.h; a backend folder defines it. The two
things the seam exists for — headless tests and an eventual second
platform — are both served by build-time selection, and neither needs two
backends live in one process. A virtual call per sprite is a tax on the
frame loop that T8 does not permit, and a compile-time choice fails at
link rather than at run time (T5). Promoting a concrete class to an
interface later is mechanical and changes no call site, so that option is
held, not spent — the same escalation as promoting a folder to a library.
Modules
Section titled “Modules”A module is a folder inside engine/, not a build target. The walls
between modules are include discipline — every include names its module
(#include "engine/collision/narrow_phase.h", see
CONVENTIONS.md) — and review enforces the direction
below. If a wall ever needs to be load-bearing, the escalation path is
known and cheap: promote the folder to a static library, as MattMath
already is (T5). That option is held, not spent.
| Module | May depend on |
|---|---|
math |
nothing — and it links nothing, which is what makes this true |
core |
math |
collision |
core, math |
render |
core, math — D3D11 inside d3d11/, D3D12 inside d3d12/, OpenGL inside gl/, Vulkan inside vulkan/, nothing inside null/ |
scene |
core, math, collision, render |
input |
core, math — XInput inside xinput/ only |
audio |
core, math — XAudio2 inside xaudio2/, nothing inside null/ |
ui |
core, math, render, input |
assets |
core, math, render, audio, rapidjson |
app |
everything |
Dependencies point one way — toward math — and never sideways in a
cycle. core is the only module everything may lean on. Three modules
lean on peers, for the same kind of reason: ui, because widgets
genuinely are rendering plus input; scene, because a scene genuinely is
an object list plus a sweep plus a fan-out; and assets, because a loader
has to know what it is building.
Scene is why scene is a folder and not a file in core/. The review
filed it as core/scene.*, written before collision/ existed. It cannot
go there: a scene owns collision objects and sweeps them, and collision
depends on core, so core → collision would close a cycle — and it
drives the renderer, which core → math does not allow. Both walls are
real, and a module is a folder, so the cheap answer is the right one.
Nothing points back at assets, and that is what keeps its arrows from
closing into a cycle. Resource types are passive: a sprite sheet is
handed its frame table and a sound bank its effect instances, already
parsed. No type on the draw path reads a file, so rapidjson reaches
assets and stops there — drawing a sprite does not compile a JSON
parser. It is a SYSTEM PRIVATE include on the engine, and that is now
literally true rather than nearly true: assets/json.h hands out
JsonDocument and JsonValue, whose only rapidjson is a void*
recovered inside json.cpp, so no engine header names the parser and no
client needs it to include one. tests/assets used to ask for rapidjson
on its own account and no longer does, which is the check. The game still
does, for one reason that will not go away: the save file is the one JSON
this project writes, and assets only reads.
JsonValue is also the module’s answer to a question every loader had
answered separately, or not at all. rapidjson’s accessors assert on a
missing key or a wrong type, NDEBUG disarms the assert, and content
files are the one input a person types by hand — so an unchecked read is
a stopped debug build and a null dereference in a shipped one. Every
accessor on JsonValue checks, and throws naming the file and the
position in it ('./levels/turbulence.json': collision_objects[17] has no 'colour'), which is T6 applied to the input most likely to be wrong.
where() hands the same position to a caller with its own reason to
reject what it read — an unknown colour name is a content mistake exactly
like a missing key.
The backend is three translation units, and all of them are in its folder.
A backend supplies renderer.cpp, render_resources.cpp and
texture_factory.cpp, plus at most one more for the part of its API that is
not about drawing, plus whatever it needs to build its shader. The third of the
three is there because exactly one step of loading content names a graphics
type — turning already-decoded bytes into a texture. Working out the path and
reading the file are render/resource_factory.cpp and the readers beside it,
written once for every backend.
What a backend needs to build its shader is not always in its folder, and
that is the one thing above it that is shared rather than written twice.
render/sprite.hlsl is compiled by three backends — at 4_0_level_9_1 for
D3D11, 5_1 for D3D12 and 6_0 for Vulkan, into three byte arrays that never
meet — because the source they compile is character for character the same
file. Two of those go through fxc and produce DXBC; the third goes through
the Vulkan SDK’s dxc and produces SPIR-V, which is a different compiler and a
different intermediate for one unchanged source. A backend owns the profile it
asks for and the binding it gives the shader’s b0 — a constant buffer, four
root constants, a uniform buffer at a shifted descriptor binding — and it does
not own the arithmetic, which is the same rule as everything else on this
page.
There are five backends: render/d3d11/, render/d3d12/, render/gl/,
render/vulkan/ and render/null/, chosen by LABRADOR_RENDER_BACKEND at
configure time. The first four are held to the same RenderPixelTests and to
the same images in tests/render/golden/; the fifth has no graphics API and
records what it was asked to draw, which is what makes drawing assertable where
there is no driver. Nothing outside any of the five folders names it, which is
what makes selection a list of files rather than a set of #ifdefs.
A fourth backend was worth having for one claim the first three could not
test. D3D11 and OpenGL both hide the CPU/GPU boundary behind a driver, and
the null backend has no GPU to be out of step with — so until D3D12 the seam
had never met an API where the engine owns synchronisation. Frames in flight,
a command allocator that may not be reused until a fence says so, a vertex
page that is still being read next frame, an upload the load path has to wait
for: all four are below the seam in render/d3d12/, and none of them reached
a line of render/renderer.h. It reaches less hardware than the backend
beside it — feature level 11_0 and Windows 10, against 10.0 — so it is a seam
test and a second rasteriser CI can run, and never a way down onto the low
tier.
The fifth is the first one that is about a platform rather than about the
seam, and it still found a term the seam had never been asked.
render/vulkan/ is here because Vulkan is the single API that reaches Android,
Linux and — through MoltenVK — the Apple platforms, so an engine that has it
has stopped needing a backend per platform; docs/port/android.md is where
that is argued and where the rest of such a port is costed. What it tested on
the way in was who says the window changed. Every other backend learns it
from Win32, and Renderer::window_size_changed is written entirely in terms of
a caller that already knows; Vulkan’s presentation engine answers
VK_ERROR_OUT_OF_DATE_KHR from an acquire or a present with no window message
anywhere near it. The contract held, because the frame is drawn into an image
the engine owns and blitted into a swapchain image at present, so what goes
stale is not what the seam calls the back buffer —
render/vulkan/device_resources.h carries that argument and it is the largest
decision in the port. It is also the one backend whose toolchain is not already
on the machine: SPIR-V at build time means the Vulkan SDK, which
cmake/compile_shaders.cmake states as the tax it is.
audio is the second module with a seam and two backends, and it is the
one that shows the rule is about modules rather than about render.
engine/audio/audio_device.h is a concrete class chosen at build time by
LABRADOR_AUDIO_BACKEND, exactly as the renderer is, with audio/xaudio2/
behind it and audio/null/ — which records what it was asked to play — as the
headless implementation PHILOSOPHY requires of every seam. What is above the
seam is the part worth stating, because it is where the cut was argued: which
name means which wave, which handles are valid, and the clamp that folds a
volume into [0,1] and a pitch and a pan into [-1,1] are all engine arithmetic
and sit above the test for whether there is anything to play. Below it is what
an audio API actually does and nothing else. The one thing the seam declines
to decide is the container: open_wave_bank takes a directory and a bank
name, never a file name, so the extension and the reader are the backend’s —
an .xwb on xaudio2/, and on null/ the list of wave names the definition
JSON gave it, because that JSON is content this engine parses and the container
is not.
A backend may publish a second header beside backend.h as long as it names
no backend type — render/null/recording.h is one, audio/null/recording.h
is the other, and both are meant to be included by tests. The rule
cmake/check_engine_includes.cmake enforces is the folder, not one filename
in it: a subfolder of render/ or of audio/ is a backend and its headers
are its own, so a file outside one that names any header in it fails the build.
It was written as backend.h by name once and that left device_resources.h
beside it as the way through. assets/ declares which manifest entries become
textures and fonts and calls render/resource_factory.h, whose declarations
name nothing a backend owns.
No file outside render/<backend>/ includes the backend header, and
cmake/check_engine_includes.cmake fails the build for one that does. The
shell is not an exception to that: it passes its window handle to
Renderer::create_device as a void*, because a window handle is not a
graphics type — it is this platform’s window, and the backend casts it back.
The rule is about any file, and it is worth saying because it was once
written as a count of implementation files. Counted that way it missed the
include that carried the backend furthest: assets/resource_loader.h named
ID3D11Device1 in a constructor, and app/application.h includes it, so
<d3d11_1.h> reached every state file in every client without anyone
choosing that. A header carries a backend further in one line than a
translation unit can, so the check reads both — and being a check rather than
a paragraph is the point (T5).
The shell asks the machine exactly one question, once, and what the
answer sizes is one thing. ApplicationOptions::max_threads defaults to
the logical-processor count and sizes the thread pool. It does not size
anything else, and it used to size two other things: the renderer’s view
capacity and — through ThreadPool::max_num_threads — the partition
count. Those are three questions with three different right answers. A
pool’s ceiling is a property of the machine; a view capacity is a property
of the layout, four for split-screen and one for a sample, on any
machine; a partition count is a property of the work. The conflation was
not untidy but expensive, because view capacity is what sizes the per-view
recording state before a frame starts, and on an integrated GPU that state
comes out of the same memory the game runs in.
app sits at the top and is the one module allowed to depend on all of
them, because assembling them is the whole of its job: it opens the
window, creates the device, constructs every service once, runs the loop
and owns the state stack. Nothing in the engine may point back at it —
that is the rule that keeps “depends on everything” from meaning “cycle
with everything”, and it is what lets a game link the engine and use as
little of app as it likes.
A game project
Section titled “A game project”For readers arriving from Unreal, the translation:
| Unreal | Labrador |
|---|---|
Project (.uproject) |
a folder: a short CMakeLists.txt, a main.cpp, a content/ directory |
Module (Build.cs) |
an engine folder with a fixed dependency direction — users don’t write modules |
| Plugin | none. Mechanisms enter the engine by design (T1), not through a plugin registry |
Content browser + .uasset |
content/: plain JSON and standard formats, walked by the manifest (T7) |
| Blueprint | C++ against the engine API (T10) |
A project is an ordinary C++ application that links two static
libraries. There is no project file format, no code generation, no
module manifest — the CMakeLists.txt is a dozen honest lines. Starting
a game is copying samples/minimal/, renaming the target, and owning
everything inside — including the starter code the
sample exists to hand over (see Structural types in PHILOSOPHY.md).
Its anatomy is the sample’s anatomy:
main.cpp— constructs the application shell, hands it the first state.- states — the game’s flow: menus, gameplay. The engine runs the state machine; the states are the game’s.
- objects — types implementing
GameObject/CollisionObjectat whatever granularity the game chooses. content/— definitions, levels, tuning, assets. Data says which and how much; the code says how (T7).- the shared settings target — linked like everyone else’s, so a game is compiled exactly as strictly as the engine that hosts it (T5).
The boundary
Section titled “The boundary”The engine provides mechanism; the game provides policy (T1). They meet at a seam made of three small interfaces and of data the engine routes but never interprets. Engine headers contain no game nouns; the litmus test lives in PHILOSOPHY.md (The boundary).
flowchart LR
subgraph ENGINE["the engine — mechanism"]
scene["Scene, game loop,<br/>fixed-step timing"]
collision["collision detection<br/>and response"]
render["renderer, cameras,<br/>viewports, split-screen"]
input["input devices:<br/>polling, deadzones, edges"]
audio["audio playback<br/>and mixing"]
assets["asset loading,<br/>registries, JSON"]
states["state machinery"]
widgets["widgets, focus,<br/>navigation"]
end
subgraph SEAM["the seam"]
interfaces["small interfaces<br/>GameObject · CollisionObject · State"]
data["uninterpreted data<br/>layers and masks · opaque tags<br/>string keys · spawn groups"]
end
subgraph GAME["the game — policy"]
match["the match: rounds,<br/>win conditions, scoring"]
meaning["what collides with what,<br/>and what a hit means"]
drawn["what is drawn: sprites,<br/>HUD, menu content"]
actions["which actions exist,<br/>and their bindings"]
sounds["which sounds play,<br/>and when"]
content["the content: definitions,<br/>levels, tuning"]
flow["the states: menu flow,<br/>gameplay flow"]
end
ENGINE --- SEAM --- GAME
Everything the game is — to the engine — is a set of states plus
content. There is no IGame to implement, no engine Player, Level,
Match or Team (Structural types, PHILOSOPHY.md): the boundary is not
just where the code splits, it is where the vocabulary splits.
Development documentation (unreleased). Built from Labrador 862e08b of 2026-10-08.