Startup
src/services/preload.ts runs an ordered list of tasks that actually await the work
the editor needs to be warm: the startup tileset, the unit tables, the GRPs a blank
map draws, then the startup document. The splash shows real progress and leaves when
the tasks are done. Do not add a task that is not awaiting something, because the
bar reaching the end is the promise that the editor is ready.
Every task is best-effort. A failure is logged as "unavailable" and stepped over, since missing game data is a normal state everywhere else.
The first frame#
The desktop shell keeps its window hidden until the renderer's first paint
(desktop/main.ts), so whatever paints first is how soon the app appears at all.
Two things keep that early:
index.htmlcarries a boot splash — the splash card as plain markup with its own inline styles, painting before the stylesheet arrives and long before the bundle evaluates. It is a copy ofstyles/splash.cssin its initial state, so change both together;SplashScreendrops it in a layout effect once it has mounted (splash/bootSplash.ts), andAppdrops it on the?nosplashpath.Appdefers the chrome by two frames. Mounting the menu bar, toolbar, docks, viewport and dialog host is one commit of well over a thousand renders, and doing it in the first commit blocked the first paint behind it. The splash paints alone, the chrome follows. The veil is opaque (.splash-veil.solid) until it is there, since there is nothing behind it to see through yet.
The saved maximized state is applied when the window is shown, not when it is created.
On Windows maximize() is a ShowWindow call: maximizing a show: false window shows it.
Doing it up front defeated the whole arrangement — the window appeared black at 140 ms and
stayed that way until the renderer painted, and every signal below found it visible already
and did nothing.
But neither Windows nor an X11 window manager applies that maximize synchronously, so the
first composited frame could still be the window at its created size in a corner, jumping to
full screen a moment later. A window that is going to be maximized is therefore created at
the work area of the display it will open on (openingBounds), which makes that frame the
right size and place. The rectangle the user actually left behind is no longer the window's
own idea of its restored size, so keepRestoreBounds holds it: it is what gets saved while
the window is still maximized, and where the first "restore down" puts the window.
The maximize is then applied MAXIMIZE_AFTER_MS (60 ms) after the window is on screen, not
as part of showing it: a maximize landing in the same frame resizes the renderer at the moment
the window appears, and what is on screen until the next frame arrives is the one painted for
the old size — the app in a small rectangle in the corner of a window that is already big.
The window is created at the size the maximize is going to give it, so the delay costs nothing
visible. startup.log carries the geometry of each step (traceBounds: created, shown,
maximized) and the renderer's own innerWidth/innerHeight a second later, which is what
tells a window that is the wrong size apart from a renderer that never got the resize.
ready-to-show is the frame the shell wants, but it is not a promise: a window that is
not on screen is not guaranteed to be composited, and on Windows one that never announced
a paint meant seconds of no window at all, followed by an editor whose splash had already
run, animated and dismissed itself where nobody could see it. So showWhenReady in
desktop/main.ts takes three signals — the paint, then dom-ready plus
SHOW_AFTER_DOM_MS (the boot splash is made of that markup, and the window's own
backgroundColor is its backdrop, so the worst case is a frame or two of flat dark), then
SHOW_LATEST_MS regardless.
The renderer no longer depends on any of that being quick. SplashScreen counts both its
minimum and maximum dwell from the moment the page is visible, not from mount — a page
in a hidden window neither animates (the card is a requestAnimationFrame loop, and those
do not run in one) nor is seen — so a launch that takes a while to put the window up still
shows the splash instead of skipping it. App's two-frame chrome deferral has a timer
behind it for the same reason.
Running the Windows build from WSL#
The desktop build cross-builds from WSL, and where the packaged app is run from changes
the launch more than anything in the code: \\wsl.localhost\… is a 9p share, and Chromium
loading 380 MB of binaries and then every asset through it is minutes, not milliseconds. A
measured comparison of the same build, from the WSL filesystem and from C::
| milestone | /mnt/c/… |
\\wsl.localhost\… |
|---|---|---|
| main script evaluated | 47 ms | 628 ms |
| window created | 142 ms | 963 ms |
dom-ready |
215 ms | not within 25 s |
So copy it over first, and launch it from there:
npm run build:desktop -- win --dir # release/win-unpacked
cp -r release/win-unpacked /mnt/c/Users/<you>/scmjs-test
SCMJS_TRACE=1 WSLENV=SCMJS_TRACE /mnt/c/Users/<you>/scmjs-test/scmJS.exe
WSLENV is what forwards an environment variable into a Windows process; SCMJS_TRACE=1
then echoes the trace to the terminal as well as the file. Note that the app's user data is
%APPDATA%\scm-js — Electron takes the folder from package.json's name, not from
electron-builder's productName — which from WSL is
/mnt/c/Users/<you>/AppData/Roaming/scm-js.
SCMJS_TRACE=1 writes the launch's milestones — process creation, main script, app ready,
window created, dom-ready, did-finish-load, and which signal showed the window — to
<userData>/startup.log (always — the file holds the last launch). "It hung for a few seconds and then opened" has several possible
causes on one machine (a virus scanner reading 380 MB of binaries, the main bundle, the
first paint) and only the timings tell them apart.
src/devReactTracks.ts disables React 19's dev-only Components performance track,
which serialises props for every render and turned mounting the chrome into about
seven seconds of blocked main thread. Set VITE_REACT_TRACKS=1 to keep it when you
want to profile renders. Production builds never had the problem.