scmJS docs

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:

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.

scmJS 0.1.0 · Generated from the repository. StarCraft and Brood War are trademarks of Blizzard Entertainment; this project ships none of their data.