Skip to content

CMake as the single source of truth: all six platforms verified, legacy projects retired#79

Merged
greenfire27 merged 46 commits into
developmentfrom
cmake-do-over
Jun 27, 2026
Merged

CMake as the single source of truth: all six platforms verified, legacy projects retired#79
greenfire27 merged 46 commits into
developmentfrom
cmake-do-over

Conversation

@greenfire27

Copy link
Copy Markdown
Collaborator

What this is

Replaces Torque2D's hand-maintained per-platform build projects with a single CMake source-of-truth build. Adding an engine file now means editing one explicit list (cmake/EngineSources.cmake / cmake/PlatformSources.cmake) and regenerating; every legacy .sln / Xcode project / Makefile / Emscripten recipe has been retired.

Platform status — all six build and are runtime-verified

Platform Built Runtime verified
Windows (VS2022) ✅ Debug/Release/Shipping ✅ editor launches
macOS (arm64) ✅ signed .app ✅ editor renders/animates
Linux (x86_64 + x86 32-bit) ✅ boots under WSLg
iOS (arm64 sim + device) ✅ signed .app ✅ on a real iPad
Web (Emscripten/WASM) ✅ editor + toys render in-browser
Android (arm64-v8a) ✅ APK ✅ boots/renders on a real Pixel 7 Pro

Per-platform configure flags and the full runtime-fix write-ups live in cmake/BUILD-PLATFORM-NOTES.md.

How it's structured

  • Root CMakeLists.txt (target-based) selects the active platform back-end and applies its libs/frameworks/defs.
  • cmake/EngineSources.cmake (cross-platform) + cmake/PlatformSources.cmake (per-OS back-ends) are the authoritative file lists — no globs.
  • Third-party libs (libogg/libvorbis/lpng/ljpeg/zlib, plus FreeType-on-wasm) build as static targets; GoogleTest is wired for the desktop unit tests.
  • Double-click generator scripts at the repo root: generate-vs2022.bat/generate-vs2026.bat, generate-xcode.command, generate-xcode-ios[-device].command, generate-make.sh / build-linux.sh, generate-emscripten.sh.

Retired (CMake replaces them)

The VS solutions, the macOS + iOS Xcode projects, the Linux Makefiles, and the Emscripten reference recipe (engine/compilers/{emscripten,cmake-modules}). engine/compilers/ now contains only android-studio — the Gradle shell that drives the root CMake via the NDK.

Load-bearing Windows settings

Static /MT for all configs (avoids _DEBUG → tinyXML #define DEBUG → Box2D breakage), /Zc:wchar_t- (so wchar_t == the engine's UTF16), C++17, _HAS_STD_BYTE=0.

Notable runtime fixes surfaced along the way

Going from "builds" to "runs" took real fixes, several cross-platform: arm64 float→unsigned saturation bugs (frozen clock/fades on macOS/iOS), a std::sort strict-weak-ordering abort, a signed-char string-hash OOB, font cache/CharInfo bounds guards, an Android module-registration path bug, the Android font-name parse (modern Windows platformID records) + a 3 MB frame allocator, and the Emscripten immediate-mode GL blending shim. Details per platform in the notes.

Known follow-ups (non-blocking, documented)

  • Android: decorative non-.uft faces (e.g. the "black ops one" titles) render blank — a per-face→Roboto fallback is deferred pending a local-device re-test.
  • Web: per-face .ttf resolution to shrink the .uft download.
  • Harmless Con::init should only be called once boot double-init (logged).

🤖 Generated with Claude Code

greenfire27 and others added 30 commits June 23, 2026 11:34
Replace the glob-based pre-"target" CMake (basics.cmake/torque2d.cmake +
libraries/*.cmake) with modern, target-based CMake that lists engine sources
EXPLICITLY, so CMake — not the filesystem or a hand-maintained .sln — becomes
the source of truth.

- cmake/EngineSources.cmake: explicit cross-platform engine source list
- cmake/PlatformSources.cmake: Win32 sources (other platforms stubbed)
- engine/lib/CMakeLists.txt: libogg/libvorbis/lpng/ljpeg/zlib as static targets
  (only the real library sources, excluding the standalone tools the old
  recursive glob wrongly compiled)
- GoogleTest built via add_subdirectory and linked for the in-engine tests

Reconciles drift vs the VS2022 project: adds 2d/editorToy and math/noise;
includes 2d/gui/guiImageButtonCtrl.cc (on disk but never added to the .sln);
drops dead sfx/spine; excludes mobile-only bitmapPvr.cc on Windows.

Load-bearing Windows settings (documented inline + in CLAUDE.md): static /MT
runtime for all configs (avoids _DEBUG -> tinyXML "#define DEBUG" -> Box2D
C1017), /Zc:wchar_t- (wchar_t == engine UTF16), C++17, _HAS_STD_BYTE=0; drops
the old spurious unconditional DEBUG=1.

Verified: configures + builds Debug and Release with the VS2022 generator and
the resulting exe launches (Project Manager UI, clean OpenGL init).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provide double-click "generate the project file" scripts for users who do not
want to learn CMake (they install CMake once; pre-built binaries still cover the
least-advanced users):
  - generate-vs2022.bat  (tested, builds + runs)
  - generate-vs2026.bat  (CMake 4.x supports the VS18 2026 generator; needs
                          VS2026 installed to generate)
  - generate-xcode.command / generate-make.sh  (scaffolded, verify on platform)

Scaffold the remaining desktop/mobile Apple + Linux platforms in the modern
CMake (ported from the old recipe / the maintained Xcode + Xcode_iOS projects;
the old CMake never actually supported iOS):
  - cmake/PlatformSources.cmake: explicit _MACOS, _LINUX, _IOS source lists
  - CMakeLists.txt: APPLE (macOS frameworks), UNIX (X11/Xft/OpenGL/FreeType via
    find_package, openal, pthread), and a distinct iOS branch (TORQUE_IOS, since
    APPLE is also true on iOS) with the UIKit/OpenGLES framework set
  - macOS/Linux/iOS are SCAFFOLDED, not yet verified on-platform

Docs and hygiene:
  - README: step-by-step VS2022 generation guide for non-CMake users
  - cmake/BUILD-PLATFORM-NOTES.md: status board + per-platform checklists and
    known gotchas (SDL2 omitted, iOS needs bitmapPvr.cc + GameKit, WSL GUI, etc.)
  - .gitattributes: fix a stray pasted line; pin LF for *.sh/*.command and CRLF
    for *.bat so the scripts work on every platform

Windows still configures and builds clean (383 TUs); the macOS/Linux/iOS blocks
are inert on Windows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… files

Rewrite PR-builds.yml so each job generates a project file from the CMake
source of truth and builds it:
  - Windows: VS2026 (windows-2025-vs2026) and VS2022, each x64 + Win32, via the
    CMake VS generators (replaces the retired VS2019 jobs and direct msbuild).
  - Linux: x86_64 and 32-bit (-m32 multilib) via the Unix Makefiles generator.
  - macOS: the Xcode generator.
  - iOS: the Xcode generator with -DCMAKE_SYSTEM_NAME=iOS, built without signing.

Each job pulls a recent CMake (lukka/get-cmake — the VS2026 generator needs
4.x), configures, builds Debug+Release, and uploads the package artifact.
Also: checkout v2->v4, add a concurrency cancel, and drop the now-unneeded
setup-msbuild / windows-sdk-install steps.

Windows is verified; macOS/Linux/iOS are freshly migrated and these jobs are
the verification loop for them (expected to need iteration). Running the unit
tests headlessly is a planned follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Android build was stale/broken: its hand-maintained ndk-build Android.mk
referenced dozens of deleted engine files. Rebuild it on the CMake source of
truth and modernize the Android Studio project.

CMake:
  - CMakeLists.txt: Android branch builds the shared library libtorque2d.so
    (OUTPUT_NAME torque2d), with the platformAndroid sources, GLES/EGL/OpenSLES/
    log/android/z links, and IMPORTED prebuilt freetype(.a)+openal(.so) for
    arm64-v8a. Desktop-only bits (gtest, testing/* sources, UNICODE defines,
    the Windows .rc) are gated off; every `UNIX AND NOT APPLE` block now also
    excludes Android (Android is UNIX).
  - cmake/PlatformSources.cmake: explicit TORQUE_PLATFORM_SOURCES_ANDROID.
  - engine/lib/CMakeLists.txt: don't build the bundled zlib on Android (lpng
    links the NDK system libz instead).

Gradle (engine/compilers/android-studio): AGP 3.5.0 -> 8.6, Gradle 5.4.1 -> 8.7,
jcenter -> google()/mavenCentral(), compileSdk 28 -> 34, add namespace,
externalNativeBuild ndkBuild -> cmake (root CMakeLists), abiFilters arm64-v8a,
ANDROID_STL c++_static + C++17, package libopenal.so via jniLibs, manifest
exported=true. Deleted the stale Android.mk/Application.mk and the committed
.cxx ndk-build cache (now gitignored).

CI: new headless Android job (gradlew assembleDebug on ubuntu with the NDK).

Target is arm64-v8a only (only ABI with prebuilt freetype/openal). macOS/Linux/
iOS/Android remain scaffolded-not-verified; Windows still configures clean (383
TUs). The Android CI job is the verification loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nManagement{})

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The global *.a / *.so gitignore rules were hiding the vendored prebuilt Android
libraries, so CI had no libfreetype.a / libopenal.so to link libtorque2d.so
against. Add .gitignore exceptions and commit the arm64-v8a binaries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Linux scaffold was never built; it failed to link on three wrong
assumptions. Fix them in the UNIX (non-Apple) block of the root CMakeLists:

  * SDL 1.2 is REQUIRED, not optional. The platformX86UNIX back-end calls
    1.2-only APIs (SDL_GetVideoSurface, SDL_WM_*, SDL_*GammaRamp,
    SDL_GL_SwapBuffers) and pulls X11_KeyToUnicode out of libSDL. This is
    NOT SDL2. Resolve it via find_library/find_path (the latter so the
    `#include <SDL/SDL.h>` style headers resolve).
  * detectX86CPUInfo comes from platform/platformCPUInfo.asm, which is
    32-bit-only NASM (does not assemble for elf64) and is referenced only
    when TORQUE_64 is undefined. So define TORQUE_64 on 64-bit (asm unneeded)
    and assemble the asm via NASM (elf32) on 32-bit.
  * Bitness macros: 64-bit gets TORQUE_64 (__amd64__ is auto); 32-bit gets
    `i386` (bare i386 isn't predefined under standard C++ but types.gcc.h's
    CPU detection keys off it). Path chosen via CMAKE_SIZEOF_VOID_P.

Verified building, linking, and running both 32- and 64-bit (Ubuntu 22.04 /
WSLg): both launch, init SDL 1.2 + OpenGL, open a window, and run the main
loop.

CI (PR-builds.yml): add libsdl1.2-dev (+ :i386) and nasm; pin both Linux jobs
to ubuntu-22.04. On 24.04, libsdl1.2-dev is the SDL2-based sdl12-compat shim,
which lacks X11_KeyToUnicode and would fail to link; 22.04 still ships genuine
SDL 1.2.15.

Docs: update BUILD-PLATFORM-NOTES.md status + Linux round, and generate-make.sh
dependency/status header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Android NDK / Linux toolchains define __aarch64__, but types.gcc.h only
recognized Apple's __arm64__, so arm64 builds failed CPU/endian detection
(Unsupported Target CPU, Endian define not set) and TORQUE_CPU_X64 went
undefined (hashTable.h pointer->U32 cast error). Match both spellings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er')

The engine uses the C++17-removed 'register' keyword in ~35 files. GCC tolerates
it but Clang (Android NDK, macOS/iOS) treats -Wregister as an error. Add
-Wno-register for non-MSVC compilers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TORQUE_CPU_X64 means 'any 64-bit CPU' and is now set for arm64 too, so the x86
SSE inline asm in mMathSSE.cc was wrongly compiled on arm64 (invalid 'd' input
constraint). Guard on the x86-specific TORQUE_CPU_X86_64 instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
T2DActivity.h includes <android_native_app_glue.h>, which is vendored in
engine/source/platformAndroid; add that dir to the Android include path (matches
the old Android.mk LOCAL_C_INCLUDES).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unner)

The Linux jobs ran `cmake --build --parallel` with no job count. For the Unix
Makefiles generator that passes `make -j` with NO limit, which starts every
ready translation unit at once — measured at 100+ concurrent g++ processes.
On a hosted runner that exhausts RAM and the OOM killer reaps the runner agent,
surfacing as "The runner has received a shutdown signal" with no compile error.
It reproducibly killed the memory-heavier 64-bit Debug build (-g); the 32-bit
Release job happened to stay just under the limit.

Pin all three Linux build invocations to `--parallel "$(nproc)"`. Windows
(VS), macOS/iOS (Xcode) generators cap at the core count by default, so only
the Make-generator Linux jobs were affected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the CMake build produce Apple Silicon binaries for both macOS and
iOS, with both the Unix Makefiles and Xcode generators.

macOS (arm64):
- zlib: define HAVE_UNISTD_H on Unix so zconf.h pulls in <unistd.h>;
  modern clang errors on the otherwise-implicit read/write/close/lseek
  in gz*.c (the old build's gnu89 C dialect masked this).
- Force-include a Cocoa prefix header (tools/CMake/macOS-Prefix.h) for
  the platformOSX .mm back-end, which uses AppKit/Foundation types at
  file scope (NSApplicationMain, NSEvent, NSCursor, ...) and relied on
  the legacy Xcode prefix header. __OBJC__-guarded, so C/C++ TUs are
  unaffected.
- Fix one stray include in osxCocoaUtilities.mm ("fileDialog.h" ->
  canonical "platform/nativeDialogs/fileDialog.h").
- Pin CMAKE_OSX_ARCHITECTURES=arm64 and CMAKE_OSX_DEPLOYMENT_TARGET=11.0
  (Big Sur floor for arm64; still supports a future Metal renderer).
  Both overridable on the command line.

iOS (arm64 simulator):
- Predefine TORQUE_OS_IOS (the engine's OS detection gates its iOS
  branch on it but only defines it inside that branch — a chicken-and-egg
  that otherwise selects the desktop-GL back-end and fails).
- Force-include a UIKit/Foundation prefix header (tools/CMake/iOS-Prefix.h).
- Define NO_REDEFINE_GL_FUNCS: the debug outline-GL macro
  (#define glDrawArrays glDrawArraysProcPtr) leaks into the modern SDK's
  gl.h (dragged in via UIKit->CoreImage) and collides the engine's
  same-named variable with the SDK's function decl. This is the engine's
  own escape hatch; outline/wireframe debug draw becomes a no-op on iOS.
- Add graphics/bitmapPvr.cc (PVR textures, excluded from desktop builds)
  and link GameKit for platformiOS/GameCenter.mm. No code-signing for the
  simulator build.

Both targets build and link with 0 errors. The desktop binary launches
without crashing; full GUI/GL runtime should be confirmed from an
interactive desktop login (it blocks on a window-server session when run
headless). Docs/status board updated in cmake/BUILD-PLATFORM-NOTES.md,
including a comparison against the legacy Xcode/Xcode_iOS projects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
zutil.h has an ancient Classic Mac OS branch that does
`#define fdopen(fd,mode) NULL` under `MACOS || TARGET_OS_MAC`. But
TARGET_OS_MAC is 1 on ALL modern Apple platforms (macOS and iOS), which
DO have a real fdopen() — so the macro clobbers the SDK's <stdio.h>
fdopen declaration (_stdio.h:318) and fails to compile.

This is what broke the macOS and iOS CI jobs (Xcode 16.4, SDK 15.5 /
iPhoneOS 18.5): the prior commit's HAVE_UNISTD_H pulls <unistd.h> in
early via zconf.h, which defines TARGET_OS_MAC before zutil.h's branch,
tripping the stub on the newer SDKs (it was latent on the older local
SDK 15.2). Guard the stub with !defined(__APPLE__) so only true Classic
Mac OS gets it; modern Apple uses the real fdopen.

Verified by reproducing the exact CI commands locally: macOS Debug+Release
(Xcode generator) and iOS device Release (CODE_SIGNING_ALLOWED=NO) both
build clean (arm64).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same Classic-Mac-OS landmine as the zlib fdopen fix: pngpriv.h includes
<fp.h> (Classic Mac OS's floating-point header, which doesn't exist on
modern macOS/iOS) whenever TARGET_OS_MAC is defined — and TARGET_OS_MAC
is 1 on all modern Apple platforms. This failed the macOS/iOS CI jobs
(SDK 15.5 / iPhoneOS 18.5) right after the zlib fix unblocked the build
and it reached libpng. Exclude modern Apple (!defined(__APPLE__)) so it
uses <math.h>.

Verified locally by forcing -DTARGET_OS_MAC=1: the original pngpriv.h
fails with "'fp.h' file not found" (the exact CI error) and the patched
one compiles clean. The other Apple-compiled vendored libs are clear
(ljpeg already handles __APPLE__; libogg/libvorbis branch only on _WIN32).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… returns child dirs

The X11 back-end's Platform::dumpDirectories diverged from the verified Win32
implementation in two ways, both of which made getDirectoryList(path) return
just the path itself instead of its immediate subdirectory names:

  1. recurseDumpDirectories was started at currentDepth=0 instead of -1. The
     child-recursion guard is `currentDepth < recurseDepth`, so the common
     depth==0 call (getDirectoryList) gave `0 < 0` == false and descended into
     no children.
  2. In noBasePath mode it pushed the base path itself for the empty-subPath
     root call, rather than only non-empty subpaths.

Symptom: the in-engine Project Selector calls getDirectoryList(getMainDotCsDir())
to enumerate top-level project folders, so on Linux it found none -- the Toy Box
(and every other project) was missing from the startup list. Affected all Linux
getDirectoryList callers, not just the selector.

Fix matches the Win32 back-end: start the recursion at -1, and in noBasePath
mode store only non-empty subpaths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resizing the window did not update the engine: the SDL_VIDEORESIZE handler only
called Game->refreshWindow() (a repaint at the old size), so the canvas stayed
locked at its original dimensions. Three parts:

  * Add SDL_RESIZABLE to the windowed GL flags. Without it SDL 1.2 fixes the
    window size and never emits SDL_VIDEORESIZE.
  * Handle SDL_VIDEORESIZE by re-establishing the GL surface at the new size.
    Unlike the Win32 back-end (where the GL drawable follows the window and
    WM_SIZE just calls Platform::setWindowSize), SDL 1.2's drawable does NOT
    track the X window, so the extra area rendered as black / drifted off-top.
    Re-setting the video mode recreates the surface and updates
    Platform::getWindowSize(), which drives the canvas extent and GL viewport.
  * Allow arbitrary windowed sizes in OpenGLDevice::setScreenMode: the
    resolution-list check now only constrains fullscreen modes (a window-manager
    drag is any size).

Re-creating the GL surface forces a texture-manager reload, so it is debounced:
the latest requested size is remembered and the surface is rebuilt only once the
drag settles (~150ms idle), giving one reload per resize gesture instead of one
per frame.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tform notes

build-linux.sh is the Linux counterpart to generate-vs2022.bat / generate-vs2026.bat.
The Windows scripts open an IDE that builds for you; with no IDE in the loop on
Linux this configures AND compiles (bounding --parallel to nproc, matching the CI
OOM fix) and leaves a runnable exe at the repo root. generate-make.sh stays the
configure-only option.

BUILD-PLATFORM-NOTES.md: mark the 64-bit Linux GUI runtime verified under WSLg,
document the script, and record the SDL dev-package gotcha (a 32-bit-prepped box
has only libsdl1.2-dev:i386, which still provides sdl-config, so the default
64-bit configure fails find_library(SDL12_LIBRARY) until libsdl1.2-dev:amd64 is
installed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engine writes console.log to the working directory (repo root) on launch via
setLogMode. It's a per-run artifact, not source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the macOS generator script up to the robustness of the Windows
(generate-vs2022.bat) and Linux (generate-make.sh) helpers, and make
"Run" from Xcode actually launch the game:

- generate-xcode.command: fall back to /Applications/CMake.app when cmake
  isn't on PATH (the macOS .dmg installs there but doesn't add it to PATH),
  check for CMakeLists.txt, keep the window open with a clear message on
  failure (incl. the full-Xcode-vs-Command-Line-Tools xcode-select hint),
  and tell the user to pick the Torque2D scheme and Build/Run.
- CMakeLists.txt: set XCODE_GENERATE_SCHEME + XCODE_SCHEME_WORKING_DIRECTORY
  to the repo root for the macOS desktop target, mirroring the existing
  VS_DEBUGGER_WORKING_DIRECTORY. The exe loads main.cs and the asset trees
  relative to the cwd, so without this, Run-from-Xcode starts in the wrong
  directory and the engine can't find main.cs.

Verified: running the script with cmake off PATH finds CMake.app, generates
build/xcode, the scheme's customWorkingDirectory is the repo root, and the
project builds clean (arm64 Debug, 0 errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bare (non-bundled) CMake executable showed "running" but never opened
a window. main.mm called NSApplicationMain, which installs the app
delegate + menu by loading the bundle's MainMenu nib (NSMainNibFile in
Info.plist). The CMake build is a plain executable with no bundle/nib, so
NSApp got no delegate — applicationDidFinishLaunching: never fired,
runTorque2D was never called, and the process sat in an empty run loop
with no window (and at 0% CPU, writing no console.log).

Bootstrap AppKit by hand when there is no MainNib bundle: become a regular
(foreground) GUI app via setActivationPolicy:Regular, install the
AppDelegate, activate, and run. The engine already creates its own NSWindow
in runTorque2D, so no nib is needed. The legacy .app path is preserved:
if Info.plist names an NSMainNibFile, we still call NSApplicationMain.

Verified: the bare exe now starts the engine within ~2s — process goes to
RN/~20% CPU (was SN/0% idle), OpenGL initializes on the M2 Pro, and the
1024x768 window screen mode is set. Builds clean with both the Makefiles
and Xcode generators (arm64, 0 errors).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unch)

GuiListBoxCtrl::compByText/compByID are std::sort predicates but were not
a strict weak ordering:
  - compByText used `res <= 0`, so equal strings compared TRUE;
  - both reversed the sort by negating the result (`!res`), which makes
    equal elements compare TRUE in the descending case.
Both violate the strict-weak-ordering requirement of std::sort. Older
libc++ ran it as undefined behaviour without complaint, but the hardened
std::sort in Xcode 16 / clang 17's libc++ detects it and calls abort() —
so the engine crashed during editor startup (ModuleManager::loadModule
Explicit -> GuiListBoxCtrl::sortByText) as soon as it actually got to run
on macOS.

Fix: compare strictly (`< 0`) and reverse by swapping operands instead of
negating, so equal elements always compare false in both directions.

Verified on macOS (arm64, both generators): the engine now runs past the
sort and loads the editor modules instead of aborting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Platform::getRealMilliseconds() did:
    return (U32)([NSDate timeIntervalSinceReferenceDate] * 1000);
timeIntervalSinceReferenceDate is seconds since 2001, so *1000 is ~8e11 ms
in 2026 — far beyond U32_MAX. Converting an out-of-range double to an
unsigned integer is undefined behaviour, and the architectures disagree:
  - x86_64 (the old build) truncates/wraps -> a changing value -> the clock
    advanced, so it happened to work;
  - arm64 (this build) saturates via fcvtzu to 0xFFFFFFFF on EVERY call, so
    the time delta was always 0 and the simulation clock never advanced.

Consequence: TimeManager posted elapsedTime=0 every frame, Sim time never
moved, and scheduled events never fired. The engine rendered the editor
background but the Project Manager UI (shown via projectSelector.schedule(
2800,"show")) never appeared, and animations were frozen.

Fix: go through U64 first — a well-defined truncation that wraps mod 2^32
(~49 days, already handled by the engine's unsigned-delta math), matching
how the x86 build behaved. Verified with a standalone arm64 test: the old
cast yields a constant 0xFFFFFFFF (delta 0); the new one advances (delta ~6
over a 5ms sleep). In-engine, the editor now initializes fully (console
trace goes from ~37 lines, idle, to thousands with the loop busy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running as a bare executable (not a .app bundle), macOS's LaunchServices/
AppleEvent check-in sometimes delivers the launch ("open application")
event more than once. A backtrace at window creation showed the chain:

    main -> [NSApplication run] -> _handleAEOpenEvent
         -> _sendFinishLaunchingNotification
         -> -[AppDelegate applicationDidFinishLaunching:]
         -> runTorque2D -> mainInitialize -> ... -> createCanvas
         -> Platform::initWindow   (a new NSWindow each time)

So applicationDidFinishLaunching: could fire multiple times, and each one
re-ran the entire engine init and spawned another window — intermittently
1 or 3 identical "Torque2D: Rocket Edition" windows depending on how many
launch events arrived (console.log only ever showed one boot because
setLogMode(2) truncates the log each time).

Guard the delegate so the engine boots exactly once per process. Verified
the normal single-boot path is unaffected (one GL init, editor loads).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The macOS target was emitted as com.apple.product-type.tool (a command-line
tool), because MACOSX_BUNDLE wasn't set. When Xcode runs a *tool* that turns
itself into a GUI app (NSApplication / setActivationPolicy:Regular), macOS
relaunches it to grant a GUI session — and relaunching a non-bundle
executable goes through LaunchServices -> Terminal, spawning extra copies of
the app. That was the real cause of the extra "Torque2D: Rocket Edition"
windows (1 from Xcode + 2 relaunched in Terminal = 3), confirmed by process-
tree tracing: the copies were children of Terminal.app/login/zsh and NONE of
the engine's own launch paths (system/NSTask/NSWorkspace) fired.

Set MACOSX_BUNDLE so the macOS desktop target builds a real Torque2D[_DEBUG]
.app with a stable LaunchServices identity (product type ...application) —
launched exactly once, no Terminal relaunch. This is what the legacy Xcode
project did.

Also ad-hoc code sign it ("Sign to Run Locally", CODE_SIGN_IDENTITY="-",
Manual style): a .app must be signed to run on Apple Silicon, but we don't
want to require an Apple developer team for a local build.

No asset repackaging needed: the .app lands at the repo root (the runtime
output dir) next to main.cs, and the engine's getExecutablePath()
(osxFileIO.mm) already searches the bundle's parent directory for main.cs.

NOTE: iOS also outputs Torque2D_DEBUG.app to the repo root but with a FLAT
(non-Contents) layout; building iOS then macOS (or vice versa) into the same
checkout leaves stray files that break codesign ("unsealed contents in the
bundle root"). Delete the stale .app when switching platforms in one tree.

Verified: product type is 'application', the bundle builds clean (arm64,
0 errors) and ad-hoc signs (Signature=adhoc) with both generators. Runtime
(window + boot) must be confirmed from an interactive Xcode session — GUI
apps can't be launched from this headless context (launchd error 153).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion)

FluidColorI::processValue (the per-tick interpolator behind fadeTo/the
editor curtains, splashes and transitions) did:

    return start + (U8)mRound((target - start) * progress);

mRound returns F32, and (target - start) is negative whenever a channel
fades toward a SMALLER value (e.g. alpha 255 -> 0). Casting that negative
float directly to U8 is undefined behaviour: arm64's fcvtzu SATURATES it to
0 (the same instruction/issue as the getRealMilliseconds clock bug), so the
delta was always 0 and every fade-OUT froze at its start value. Fade-IN
(positive delta) worked, which is why it was easy to miss.

Concretely: the Project Manager's "torqueCurtain" (a full-screen editorBG
sprite shown on top, then faded to alpha 0 to reveal the UI) never faded,
so the editor booted to a window that only showed the background — the
actual Project Manager was fully rendered underneath the stuck curtain.

Fix: round to a signed int and cast only the final, in-range sum to U8, so
the negative intermediate is never converted to an unsigned type.

Verified on arm64: the old expression yields 255 (stuck) for a 255->0 step;
the fixed one yields 239 (progressing). The S32/F32 processValue overloads
were already safe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update BUILD-PLATFORM-NOTES.md to reflect that macOS (arm64) now builds,
code-signs, launches as a single window, boots, and the editor renders +
animates — verified from Xcode. The notes still said "GUI runtime
unconfirmed".

Documents the six runtime bugs found between "it builds" and "the editor
works" (AppKit bootstrap, std::sort comparator crash, frozen sim clock,
duplicate windows from the tool product type, codesign/bundle, frozen
fade-outs), and calls out the recurring arm64 trap (float->unsigned
SATURATES on arm64 where x86 wrapped) so the pending iOS runtime work
knows to look for it. Also flags the shared Torque2D_DEBUG.app output path
between the iOS (flat) and macOS (Contents/) builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…note

The 32-bit Make build now builds, links, and runs under WSLg (boots + GL init,
toybox/project list and window resize confirmed). It falls back to llvmpipe
(software GL) because WSLg's hardware-GL passthrough is 64-bit only.

Also corrects an earlier inaccuracy: libsdl1.2-dev:amd64 and :i386 do NOT
coexist -- they conflict on shared files, so installing one removes the other.
Records the non-destructive 32-bit workaround (point SDL12_LIBRARY at the i386
runtime libSDL-1.2.so.0, since the i386 dev symlink is gone once the amd64 -dev
is installed) and the full -m32 configure recipe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
greenfire27 and others added 16 commits June 24, 2026 10:07
…ce of truth)

With Windows, macOS, and Linux (32 & 64-bit) all CMake-runtime-verified, the
hand-maintained desktop project files are redundant. Delete them from
engine/compilers/:
  - VisualStudio 2019 / VisualStudio 2022 (the .sln + .vcxproj tree)
  - Xcode (the macOS desktop project)
  - Make-32bit / Make-64bit (the Linux Makefiles)

CMake (root CMakeLists.txt + cmake/EngineSources.cmake +
cmake/PlatformSources.cmake) replaces all of them. Kept under engine/compilers/:
android-studio (the Gradle shell that drives CMake via the NDK), and Xcode_iOS +
emscripten as reference recipes until those platforms are CMake-runtime-verified.
cmake-modules is retained because emscripten/CMakeLists.txt includes CopyFiles
from it.

Docs:
  - CLAUDE.md: rewrite the Building section to make CMake the single source of
    truth; drop the "also add new sources to the per-platform project files"
    dual-maintenance rule (there are no such files to sync anymore).
  - cmake/BUILD-PLATFORM-NOTES.md: record the retirement; note the macOS
    comparison refers to the now-deleted Xcode project (see git history).
  - .gitignore: drop the stale Make-32bit/Make-64bit ignore rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…untime fixes)

The CMake iOS target built and linked but produced a bundle-incomplete .app that
launched into a black screen. Make it a real, runnable iOS app and fix the arm64
runtime landmines that surfaced once it actually ran (editor renders, touch works;
user-verified in the iOS 18.2 simulator on an iPad Pro M4).

Bundle (CMakeLists.txt + tools/CMake):
- iOS-Info.plist.in via MACOSX_BUNDLE_INFO_PLIST — the app's window comes from a
  Main storyboard (T2DAppDelegate creates none in code); without UIMainStoryboardFile
  + a bundle id + orientations the app had no window.
- Bundle the iPhone/iPad GLKit storyboards (copied from the legacy Xcode_iOS project
  into tools/CMake so the build is self-contained) + a static LaunchScreen (modern
  iOS needs one for a full-screen drawable).
- POST_BUILD copy of main.cs + editor/library/toybox/tools into the flat .app (an
  installed iOS app is sandboxed and resolves content inside the bundle).

Runtime fixes:
- iOSTime.mm getRealMilliseconds(): arm64 float->unsigned saturation (the macOS
  osxTime.mm bug class, in iOS's own time file) -> frozen clock -> black screen.
  Compute in U64, cast last.
- defaultGame.cc: iOS FrameAllocator was 512KB (2013-era) but main.cs boots the
  desktop editor; a boot alloc overran it and tripped the fatal assert. Bump iOS to
  the desktop 3MB.
- Point-based rendering (Retina packs 2-3x pixels into the same space, so pixel-sized
  GUIs render half-size): iOSWindow.mm uses point bounds for $pref::iOS::Width/Height,
  T2DViewController.mm pins contentScaleFactor=1 before createFramebuffer and disables
  retina touch scaling -> logical res, GL backing, and touch all share one point space
  (correct GUI size + scene picking).
- Touch release (press stuck): iOSInput.mm createMouseUpEvent now falls back to the
  first occupied slot (simulator mouse-up lands a pixel off the stored coord), sets the
  cursor, and frees the slot; guiCanvas.cc rootScreenTouchUp now routes the up to the
  captured control first like the desktop rootMouseUp (a button mouseLock()s on down,
  so it never released otherwise). Touch-only path; desktop unaffected.

Tooling:
- generate-xcode-ios.command (arm64 simulator) and generate-xcode-ios-device.command
  (code-signed device). CMakeLists detects simulator-vs-device from the SDK: simulator
  keeps CODE_SIGNING_ALLOWED=NO; device uses CODE_SIGN_STYLE=Automatic with the team and
  bundle id from overridable cache vars (TORQUE_IOS_TEAM, TORQUE_IOS_BUNDLE_ID).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
iOS builds, runs, and is runtime-verified from CMake on both the simulator and a
real device, and the build is self-contained (the storyboards + Info.plist were
copied out of engine/compilers/Xcode_iOS into tools/CMake in the iOS round). The
legacy iOS Xcode project is now redundant, like the desktop projects retired
earlier — delete engine/compilers/Xcode_iOS.

What remains under engine/compilers/ is android-studio (the Gradle shell that
drives CMake via the NDK) and emscripten (a reference recipe until the Web target
is CMake-runtime-verified), plus cmake-modules (used by emscripten/CMakeLists.txt).

Docs:
- README.md: the "Building the Source" section still advertised the deleted
  per-platform projects (VS 2019/2022, OSX Xcode, Linux Make, Xcode_iOS) as
  "provided in engine/compilers" with CMake as a mere alternative. Rewrite it so
  CMake is the source of truth, listing the per-platform generator scripts and
  pointing to the wiki Building guide.
- CLAUDE.md + cmake/BUILD-PLATFORM-NOTES.md: drop Xcode_iOS from the "kept under
  engine/compilers" notes; emscripten is now the only reference recipe. Mark the
  iOS-project comparison note as retired (see git history).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
First Android runtime bug, found by running the APK on a real device via Firebase
Test Lab and symbolicating the tombstone with ndk-stack.

Root cause: Android's process cwd is "/", so defaultGame.cc resolved the boot
script to "/main.cs", then chopped the filename at the LEADING slash — leaving an
empty string as the main.cs directory. That empty dir then flowed in as the cwd
for every Platform::makeFullPathName(), making
  endptr = buffer + dStrlen("") - 1 = buffer - 1
(point before the buffer) and corrupting the path-building pointer math →
out-of-bounds write → SIGSEGV in catPath() during ModuleManager::registerModule.
Desktop never hit it because main.cs lives in a deep path with interior slashes.

Fixes:
- defaultGame.cc: when the boot script is at the filesystem root, keep "/" as the
  main.cs directory instead of truncating to "". (The real fix.)
- platformFileIO.cc: harden makeFullPathName()/catPath() against a degenerate cwd —
  compute the remaining length signed and clamp with getMax(...,0), and no-op
  catPath when there's no room (len < 3). A latent cross-platform buffer-overflow
  that a "" cwd exposed; can't overrun the buffer now regardless of inputs.

With this, Android registers all editor modules and boots into the editor. It then
crashes in font init (AndroidFont::getCharInfo null-derefs a failed FT_New_Face) —
the next bug, documented in cmake/BUILD-PLATFORM-NOTES.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
Wires Emscripten/WebAssembly into the SHARED EngineSources.cmake (the Android
pattern: one source list + guards, not the legacy hand-maintained flat list).
emcc now builds Torque2D_DEBUG.{html,js,wasm,data} via `emcmake`, and it boots
in a browser: WebGL 1.0 context comes up, all subsystems init, the editor
modules load, and the main loop runs (verified headless via Playwright +
`python -m http.server`).

Build wiring (CMakeLists.txt, PlatformSources.cmake, generate-emscripten.sh):
- TORQUE_PLATFORM_SOURCES_EMSCRIPTEN + an EMSCRIPTEN dispatch branch (matched
  before UNIX, like Android, since the toolchain sets UNIX=1).
- EMSCRIPTEN=1 defined GLOBALLY (emcc only predefines __EMSCRIPTEN__, but the
  engine's types.gcc.h AND vendored ljpeg/jconfig.h key off bare EMSCRIPTEN).
- Net swap: platformNet_Emscripten.cpp replaces the BSD-socket impl.
- emcc flags: -sUSE_SDL=1, -sLEGACY_GL_EMULATION=1, INITIAL_MEMORY+growth,
  --js-library platform.js, --preload-file for the script/asset trees. tools/ is
  excluded from the preload (build-time-only doxygen/TexturePacker), trimming the
  .data bundle 272MB -> 186MB.

Back-end compile/link fixes (years of interface drift in platformEmscripten/*):
string return types (U32 not dsize_t), a stale platState.engine assert, an
include typo, the missing getVerticalSync override (class was abstract), an F64
include, a glArrayElement no-op (LEGACY_GL_EMULATION lacks it), and the net
stub's chicken-and-egg self-guard (#if TORQUE_OS_EMSCRIPTEN before the include
that defines it — same class as the iOS TORQUE_OS_IOS fix).

Runtime fixes:
- EmscriptenFileio recurseDumpDirectories dropped the first char of each scanned
  entry ("/editor/ssetAdmin"), so editor modules never loaded — an off-by-one in
  the path join (same family as the Android leading-slash bug; web cwd is "/").
- platform.js alerted raw wasm pointers instead of strings (missing UTF8ToString)
  and used blocking alert() — the per-assert Platform alert wedged the tab in a
  dialog storm. Decode the string; route informational AlertOK to console.
- gFont.cc GFont::create hard-crashed (wasm null deref) on a missing font; its
  caller already null-checks, so it now returns a null Resource. Cross-platform
  robustness fix, analogous to the open Android font bug.

Remaining blocker: FONTS. The web build has no font backend
(EmscriptenFont::createPlatformFont returns NULL) and the editor GUI hard-asserts
without one (guiTypes.cc:768), so the canvas is black. Same root cause as the
open Android font crash — to be fixed together next. Full writeup in
cmake/BUILD-PLATFORM-NOTES.md (Emscripten round + status board).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
The Emscripten build booted but showed a black screen: the editor GUI needs a
font and the web has no font backend. Wire fonts CACHE-FIRST (the engine's .uft
glyph caches are self-contained and already in the preload) so the Project
Manager renders real text ("TORQUE2D", version, project-tile labels) plus
sprites — stable, no crash. First end-to-end exercise of the GL draw path on web
(text quads + sprite batches under LEGACY_GL_EMULATION). No FreeType, no extra
download.

Most fixes are cross-platform robustness; the asset-path ones are web-specific:

- platformAssert.cc: on TORQUE_OS_EMSCRIPTEN, log-and-continue instead of a
  blocking AlertRetry/AlertOKCancel (native confirm()) + forceShutdown(1). A
  blocking modal inside the rAF loop wedges the tab, and a per-frame assert made
  an un-dismissable dialog storm. This unblocked boot past the (harmless,
  non-fatal) "Con::init should only be called once" double-init assert.
- defaultGame.cc: give Emscripten the 3MB frame allocator (like iOS/desktop) — it
  boots the same desktop-class editor; only Android keeps the 512KB budget.
- guiTypes.cc GuiControlProfile::getFont(): replace the AssertFatal-on-null with a
  graceful fallback to another loaded size (text-render sites deref the result).
- guiProfiles.cs (AppCore + EditorCore): web ($platformUnixType=="emscripten")
  picked "monaco" (no .uft, no system font); use "share tech mono". The base
  GuiDefaultProfile also hardcoded a non-existent ^EditorCore/gui/fonts dir
  (desktop only survived via createPlatformFont); point it at an EXPANDED real dir
  under the registered ^EditorCore expando that ships the face
  (^EditorCore/Themes/LabCoat/fonts) — the resource manager doesn't resolve the
  ^Module expando for cache lookups and ^AppCore isn't loaded at editor boot.
- gFont.h GFont::getTextureHandle(): bounds-check mTextureSheets[index]. An OOB
  index returned a garbage TextureHandle whose non-NULL object was dereferenced by
  lock() -> fatal wasm "memory access out of bounds" in dglDrawText. Return a NULL
  (lock-safe) handle so an unrenderable glyph is skipped.
- AndroidFont.cpp: create() now propagates FT_New_Face failure (was return true
  even on failure) and getCharInfo() guards the face, fixing the open Android font
  crash (createPlatformFont -> null -> handled by getFont above). Shared root cause.
- gFont.cc: log the looked-for path on a font-cache miss (diagnostic).

Build gotcha (documented in BUILD-PLATFORM-NOTES): the --preload-file asset trees
aren't tracked as CMake deps, so a .cs/asset edit needs a forced repackage
(rm build/emscripten/Torque2D_DEBUG.{html,js,wasm,data} then rebuild).

Residual (minor, non-fatal): a few un-baked editor title sizes (black ops one
21/28) log a Vector-out-of-bounds per frame — debug-only (AssertFatal compiles out
of Shipping); the editor renders and is responsive. FreeType-on-wasm (arbitrary
fonts + smaller download) and Android text rendering remain tracked follow-ups.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…render)

The web build previously rendered only the pre-baked .uft sizes; anything else
degraded to a near size or to nothing (Windows' GDI is a universal backstop that
synthesizes any face+size; the web had none). This compiles the vendored FreeType
(2.4.12) to wasm and implements EmscriptenFont on it, rasterizing a bundled Roboto
.ttf for any face/size not in the .uft cache. The editor now renders ALL its text
(the previously-blank "New Project" heading/body now draw), the per-frame Vector
OOB is gone, and web matches desktop font behavior. .uft is kept (designed faces
still use it at baked sizes; FreeType only fills gaps).

- engine/lib/CMakeLists.txt: a `freetype` STATIC target, gated if(EMSCRIPTEN)
  (desktop uses system fonts/find_package, Android the prebuilt .a). Built from the
  vendored freetype-2.4.12 aggregator .c files (base/sfnt/truetype/smooth/raster/
  autofit/psnames/psaux/pshinter), FT2_BUILD_LIBRARY defined. Linked in the root
  EMSCRIPTEN block (+~1 MB wasm).
- ftmodule.h trimmed to the TrueType path we compile — ftinit.c registers every
  driver listed there, so the default full list link-errored on bdf/pcf/t42/winfnt/
  type1/cff/cid/pfr. Safe to edit: that source is only consumed by this from-source
  Emscripten build (Android links the prebuilt .a).
- EmscriptenFont.{h,cpp}: mirrors AndroidFont — FT_Init, FT_New_Face on the bundled
  .ttf path, FT_Set_Pixel_Sizes, getCharInfo via FT_Load_Char(FT_LOAD_RENDER). Uses
  the rendered bitmap dims (width/rows, stride=pitch) so alloc/copy/size stay
  consistent (metrics width can be a pixel narrower -> overrun).
- Fallback resolution honors the app/editor font separation: EmscriptenFont reads
  $pref::Web::fallbackFont, which each core registers to its OWN Roboto copy
  (AppCore -> ^AppCore/fonts, EditorCore -> ^EditorCore/gui/fonts). A shipped game
  with editor/ removed falls back to AppCore's; the editor never reaches into the app.
- Roboto-Regular.ttf (SIL OFL) bundled in both font dirs (preloaded into the web
  .data) and android-studio assets/fonts/.
- Android wired but NOT tested this round: guiProfiles request "Roboto" (the long-
  gone "Droid" isn't on modern devices) + the bundled asset; AndroidFont already
  rasterizes. APK run deferred.

Verified on web (headless Chromium): font-create failures 0, OOB ~thousands -> 1,
no crash; screenshot shows the full Project Manager tile text rendered via FreeType.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…or finds the toybox

Platform::dumpDirectories started the recursion at currentDepth 0; the child-
recursion guard is `currentDepth < recurseDepth`, so the common depth==0 call
(getDirectoryList) gave `0 < 0` == false and descended into NO children, returning
an empty list. The editor's project selector enumerates getMainDotCsDir() with
getDirectoryList() to find project folders, so on web it found none and never listed
the toybox (the only default project) — only the "New Project" placeholder showed.

Start the recursion at -1, matching the Win32/x86UNIX back-ends (this exact fix was
applied to x86UNIX during the Linux round but never propagated to Emscripten). Now
getDirectoryList("/") enumerates the immediate child dirs and the Toy Box project
card appears. Verified in-browser.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…t of bounds

_StringTable::hashString/hashStringn indexed the 256-entry sgHashTable with a
plain `char`. `char` is signed (clang/emcc default), so any byte >= 0x80 made the
index negative -> sgHashTable[negative] reads before the array. On desktop that
silently read adjacent memory (a wrong-but-harmless hash); on wasm it's a hard
"memory access out of bounds" trap.

Reproduced on web: pressing the Ctrl key while a GuiTextEditCtrl/GuiConsoleEditCtrl
has focus (the console box, or the editor's project-manager screen) delivered a
high-valued ascii that got inserted as text, then hashed on insert into the
StringTable -> instant crash:
  hashString <- StringTable::insert <- GuiControl::setText <- GuiTextEditCtrl::
  setText <- handleCharacterInput <- GuiConsoleEditCtrl::onKeyDown <- ...
This would equally crash on any high-bit input on web (e.g. typing an accented
character). Cast the index to (U8) in both hashers. Verified: pressing Ctrl in the
web build no longer freezes; the page stays responsive.

Note (not fixed here, minor): the Emscripten input layer hands a modifier-only
Ctrl press a non-zero `ascii`, so it's treated as a character at all -- worth
filtering in EmscriptenInput later, but the hash fix makes it (and all high-bit
input) safe regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…(no more phantom glyphs)

EmscriptenInputManager::MapKey assigned the raw SDL keysym as each key's ascii, so
modifiers (SDLK_LCTRL=306, etc.), function keys, arrows, keypad and locks all carried
a bogus, non-zero ascii. Pressing them was then treated as character input and
inserted a phantom (unrenderable) glyph into focused text fields -- e.g. tapping Ctrl
typed a stray box in the console edit. (The earlier StringTable hash fix stopped the
hard crash this caused; this removes the bogus character itself.)

The desktop x86UNIX back-end avoids this by deriving the ascii from
X11_KeyToUnicode(), which returns 0 for non-character keys -- but emscripten's SDL1
port has no working X11_KeyToUnicode, which is why this code used the keysym + a manual
shift switch instead. Filter the default assignment: only printable-ASCII keysyms
(0x20-0x7E) carry a character ascii; SDL special keys (>= 0x100) and the 0x7F-0xFF
range map to 0. Control keys (< 0x20: Tab/Enter/Backspace/Esc) also map to 0 and stay
handled by keycode. Shifted variants are still set by the switch below.

Verified in-browser (toybox -> console): pressing Ctrl/Alt/Shift inserts nothing,
letters still type, Backspace still edits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…trancy OOB)

ProcessMessages() polled SDL events into the shared gPlatState.eventList, cached
its size, then indexed the shared list in the loop. Handling an SDL_USEREVENT
(SETVIDEOMODE) calls SetAppState() -> Input::reactivate(), which re-enters
ProcessMessages and clears+refills that same gPlatState.eventList -- so the outer
loop then read past the now-smaller vector: Vector<SDL_Event>::operator[] out of
bounds, twice per toybox load (the two red "vector.h @ 578" fatals seen in the
console). Non-fatal since the prior assert-handling change, but a real OOB read.

Iterate a LOCAL copy of the frame's events so re-entrancy can't corrupt the
iteration. Root-caused with a temporary emscripten_log(EM_LOG_C_STACK) in the
assert path; stack pointed straight at ProcessMessages -> Vector<SDL_Event>::
operator[]. Verified: loading the toybox now logs zero vector.h:578 fatals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
LightObject::sceneRender disabled GL_BLEND between glBegin/glEnd. On desktop
GL that call is illegal inside a begin/end block (GL_INVALID_OPERATION) and is
silently ignored, so the light fan still draws with additive blending and fades
out correctly. Under Emscripten's LEGACY_GL_EMULATION the immediate-mode geometry
is buffered and the real glDrawArrays is deferred to glEnd(), so the early
glDisable(GL_BLEND) actually takes effect and the fan renders opaque -- fading to
black instead of fading out. Move the disable after glEnd().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
… & blending rounds

Marks the Emscripten/Web round DONE in the status board and documents the work
after the font rounds: the interaction round (project selector depth-0 dir scan,
signed-char StringTable hash OOB, phantom-glyph modifier keys, ProcessMessages
event re-entrancy OOB) and the blending round (LightObject glDisable(GL_BLEND)
inside glBegin/glEnd, which the web's deferred immediate-mode draw honored).
Also updates the "legacy projects retired" note: the emscripten reference recipe
is now superseded since the Web target builds via emcmake + PlatformSources.cmake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
…ocator)

The Android editor now boots, renders and runs on a real Pixel 7 Pro
(verified via Firebase Test Lab) — the last platform in the CMake
source-of-truth migration to be runtime-verified.

Two root-cause fixes, each of which exposed the next crash in the boot
sequence:

- FontManager.java (TTFAnalyzer): only read Macintosh (platformID==1)
  TrueType name records, but the bundled Roboto and modern Android system
  fonts ship only Windows (platformID==3, UTF-16BE) records. The enumerated
  font map came up empty, getFont("Roboto") returned null, AndroidFont
  failed, GFont::create returned NULL, and GuiMenuBarCtrl dereferenced the
  NULL font -> SIGSEGV. Accept platformID 1/3/0 and decode UTF-16BE for the
  Windows/Unicode records.

- defaultGame.cc (FrameAllocator): Android ran on a 512KB frame allocator
  while the desktop-class editor it boots needs 3MB (the same fix already
  applied to iOS and Emscripten). Once fonts loaded, reading a cached .uft
  glyph table overran it -> FrameAllocator::alloc SEGV. Collapsed the now
  redundant per-platform split to give every platform the 3MB buffer.

Main UI (Roboto) text renders. Remaining cosmetic gap: un-baked decorative
faces (e.g. "black ops one" titles) with no .uft and no matching
installed/bundled .ttf still render blank. See
cmake/BUILD-PLATFORM-NOTES.md (Android round) for the full write-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
The per-face -> Roboto fallback for un-baked decorative faces (e.g. the
"black ops one" editor titles) was attempted twice -- once in C++
(AndroidFont::create, an extra getFontPath JNI call) and once in Java
(FontManager.getFont returning Roboto instead of null) -- and both were
reverted. Each FTL run went silent right after FileWalker with zero
Torque2D-tagged lines and no crash; this is ambiguous (likely lossy FTL
logcat capture vs. a real hang) and unprovable from n=1 FTL logs. Since
it's cosmetic and the verified-working state already ships, the fallback
is deferred pending a re-attempt on a local arm64 device with reliable
adb logcat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
With the Web/WASM target now CMake-runtime-verified via the root
CMakeLists.txt + generate-emscripten.sh, the last hand-maintained build
recipe under engine/compilers/ is obsolete. Remove engine/compilers/emscripten
and engine/compilers/cmake-modules (the latter only provided CopyFiles for
that recipe; nothing in the active build references either). engine/compilers/
now contains only android-studio, the Gradle shell that drives the root CMake
via the NDK.

Refresh the docs to match the finished migration:
- README.md: Web is available (generate-emscripten.sh), not "not yet available".
- CLAUDE.md: all six back-ends wired+verified; emscripten retired (was "stubbed"
  / "kept until the Web target is CMake-runtime-verified").
- CMakeLists.txt: drop the dangling reference to the now-deleted recipe path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6jsrTtG7fhRRRYG9NvHZW
@greenfire27 greenfire27 merged commit b37e5dd into development Jun 27, 2026
18 checks passed
@greenfire27 greenfire27 deleted the cmake-do-over branch June 27, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant