==============================================================================
OPENVMS PORT CHANGES - ScummVM 1.8.1 Kyra + Sierra (AGI/SCI) extract
==============================================================================
Target : OpenVMS 8.4 Alpha, HP C++ (CXX) V7.3-009, DECwindows X11, SDL 1.2
Build  : DESCRIP.MMS (MMK/MMS).  No sound, no launcher.  482 objects, TWO
         images - KYRA.EXE and SIERRA.EXE.  See section 13 for the AGI/SCI
         addition; sections 1-12 predate it and describe the Kyra-only tree.
Docs   : DESCRIP.MMS holds the RULES; DESCRIP.TXT holds ALL of its commentary,
         in the same order, with the rules it describes echoed inline (indented
         four spaces) for orientation.  Split 2026-08-28 - the file had ~1100
         comment lines around ~450 of rules.  Change a rule in the .MMS, change
         WHY it exists in the .TXT, and do not put comments back in the .MMS.
Guards : __VMS                    = platform (compiler / RTL / OS)
         KYRA_STANDALONE          = this build's mode (no launcher, no sound).
                                    Misnomer now - it means "standalone
                                    loader" and applies to SIERRA.EXE too.
         STANDALONE_TARGET_SIERRA = the SECOND compile of base/plugins.cpp
                                    only; selects which engines get linked.
Note   : PROBE_*.CPP are standalone diagnostics, not part of the image.
         Build flag workarounds are NOT listed here - see DESCRIP.TXT, which
         documents every per-file /NOOPTIMIZE, LEVEL=1 and /NODEBUG rule.
         (DESCRIP.MMS itself is now bare rules; ALL of its commentary lives in
         DESCRIP.TXT, in the same order.  Read the two side by side.)
         BUT READ SECTION 12 BEFORE ADDING A FLAG: /NOOPTIMIZE is not free and
         caused a crash it was added to prevent.
Files  : one file is NEW rather than modified - engines/kyra/screen_delta.cpp
         (section 1b).  One is GENERATED - engines/vms_logo_data.h (section 11).
         Rebuild helper: REBUILD_LOGO.COM (the section 10 saves fix had one too,
         REBUILD_SAVES.COM; it has served its purpose and is gone).
Opt    : $(OPT) in DESCRIP.MMS is the single optimization setting for the tree.
         RAISED 2026-08-28 from /OPTIMIZE=(LEVEL=3,TUNE=HOST) to LEVEL=4 - the
         compiler's own default, i.e. the tree had been pinned BELOW default -
         because the build host is now real AlphaServer hardware rather than the
         es40 emulator.  Eight files still override it per-file; all eight are
         back-end BUG workarounds and none was made obsolete by the faster host.
         engines/kyra/screen.cpp (the blitter) was PROMOTED to full $(OPT): its
         LEVEL=1 rule existed only because the LEVEL=3 optimizer GROUND under the
         emulator, which is not a reason any more.  Read the $(OPT) comment block
         in DESCRIP.TXT before raising it further - it explains why LEVEL=5 and
         /ARCHITECTURE=HOST are left off, and why the real risk of a higher level
         here is a silent MISCOMPILE (section 1, common/memory.h) rather than a
         compiler crash.
MMK    : DESCRIP.MMS is NOT a dependency of any target, so editing it rebuilds
         NOTHING - delete the .OBJ by hand.  THIS APPLIES TO $(OPT) TOO: changing
         the optimization level rebuilds nothing and MMK reports the tree up to
         date, so that one change DOES need MMK CLEAN or you get a build
         description that says LEVEL=4 and an image that is still LEVEL=3.
         Headers not in $(GLOBAL_HDRS) are invisible to MMK the same way.  Use
         MMK /IGNORE=WARNING - a warning-severity compile otherwise ABORTS the
         target.  482 objects is ~8 hours under the EMULATOR and far less on real
         hardware, so "never MMK CLEAN" is now advice, not law.  Section 13 lists
         the FIVE objects to delete by hand for an incremental Sierra upgrade -
         which you can skip entirely if you are doing the full LEVEL=4 rebuild.

------------------------------------------------------------------------------
1. COMPILER BUGS / LANGUAGE LIMITS  (__VMS)
------------------------------------------------------------------------------
common/memory.h          uninitialized_copy() rewritten as an explicit loop.
                         CXX at /OPTIMIZE=LEVEL=3 MISCOMPILES the upstream
                         "new ((void*)dst++) Type(*first++)" one-liner into an
                         INFINITE LOOP: it drops the first++ increment and
                         hoists the exit test out of the loop.  Proven from the
                         /MACHINE_CODE listing.  Symptom was a wild ACCVIO at a
                         page boundary.  DO NOT RESTORE THE ONE-LINER.

common/algorithm.h       Same class of bug: ++/-- moved OUT of the assignment
                         expressions in two copy loops.

common/singleton.h       CXX cannot emit a static DATA member of a class
                         template.  The pointer now lives inside a static
                         member FUNCTION (_singletonRef()) instead.  A uniform
                         accessor keeps every platform spelled the same.

common/hashmap.h         USE_HASHMAP_MEMORY_POOL is UNDEFINED on __VMS.  The
                         ObjectPool path (placement new + explicit ~T()) crashes
                         the Alpha back-end with %GEM-F-ASSERTION in
                         GEM_CX_EXTEND_LIFETIME when the value type has a
                         non-trivial destructor (FileMap, ADFilePropertiesMap).
                         Not fixable by /NOOPTIMIZE.

graphics/scaler/scale2x.h   #define __restrict__ away - CXX does not know the
graphics/scaler/scale3x.h   GCC __restrict__ qualifier.

common/scummsys.h        scumm_va_copy = plain assignment.  On Alpha CXX
                         (pre-C99) va_list is a scalar, so this is correct -
                         same branch as MSVC.

config.h                 int64/uint64 are "long long", NOT "long".  OpenVMS
                         Alpha defaults to the LP32 data model, so "long" is
                         only 32 bits and the upstream typedef would silently
                         make every 64-bit type 32-bit and truncate rather than
                         fail to compile.  "long long" is a true 64-bit type on
                         both this target and the Linux validation host.

common/math.h            `register` dropped from intLog2().  A NO-OP: register
graphics/VectorRendererSpec.cpp   is only a hint in C++ and cannot change
                         codegen.  Recorded because the theory behind it -
                         "a long-lived register local crashes GEM_CX_EXTEND_
                         LIFETIME" - is DISPROVEN (see section 12), and
                         graphics/scaler/hq2x.cpp:106 declares nine `register
                         int`s and compiles fine at LEVEL=3.  Do NOT strip
                         `register` elsewhere expecting it to fix a crash.

------------------------------------------------------------------------------
1b. TEMPLATE-INSTANTIATION CODEGEN CRASHES  (__VMS) - the biggest single class
------------------------------------------------------------------------------
The most common way this compiler fails on this codebase.  The fault fires while
EMITTING a template instantiation, not while compiling the routine that uses it,
so the traceback names generate_one_instantiation_output_file and often no source
line at all - and /NOOPTIMIZE does NOT help, because GEM_CX (the code expander)
and GEM_ST_GET_NAME/GEM_OM_WRITE_GLOBALS (symbol emission at module
finalization) run at every optimization level.  Two recurring triggers:

  (i)  Common::find() - a free template FUNCTION.  Crashes with
       %GEM-F-ASSERTION in GEM_CX_EXTEND_LIFETIME, or in GEM_ST_GET_NAME <-
       GEM_OM_WRITE_GLOBALS <- GEM_OM_MODULE_FINI while writing the
       instantiation's external symbol NAME.
  (ii) Common::Array<T*> - an Array whose element type is a POINTER.  Array's
       freeStorage/pop_back emit a pseudo-destructor call ptr->~T() on a pointer
       type, which the back-end cannot handle.

FIX PATTERN: replace with plain C constructs that instantiate no template.
Behaviour is identical in every case below; none of these is a semantic change.

common/archive.cpp       addSubDirectoriesMatching(): Common::find(begin,end,'/')
                         -> explicit while loop over the iterators.
graphics/fontman.cpp     assignFontToName(): Common::find over _ownedFonts ->
                         indexed linear search.
engines/kyra/screen.cpp  Screen ctor: counted distinct _pageMapping values with
                         Common::Array<uint8> + Common::find -> plain
                         seenPage[SCREEN_PAGE_NUM] dedup.  Safe because the
                         values index _pagePtrs, so all are < SCREEN_PAGE_NUM.
engines/kyra/screen.h    _screenPalette's Common::Array<Palette *> -> raw
                         Palette **_palettes + int _paletteCount.  Size is fixed
                         at construction and never resized.
engines/kyra/eobcommon.h Common::Array<const int16 *> -> const int16
                         *_dscWallMapping[36].  The ctor push_back()s 36 entries
                         in ONE routine; that plus the pointer-Array pseudo-
                         destructor crashed TWO passes - %SYSTEM-F-ACCVIO in
                         GEM_SR at /OPTIMIZE and %GEM-F-ASSERTION in
                         GEM_CX_EXTEND_LIFETIME at /NOOPTIMIZE.  Fixed 36-entry
                         table, ctor-filled, read by index.

DE-TEMPLATING (same cause, applied to the port's own code):
engines/kyra/screen.h    decodeFrameDelta / decodeFrameDeltaPage were
engines/kyra/screen_delta.cpp   template<bool noXor>; noXor is now a plain
                         runtime bool parameter.  The compile-time branch was
                         only a hot-loop micro-opt.
graphics/VectorRendererSpec.h   VectorRendererSpec is NO LONGER A TEMPLATE -
                         PixelType is a `typedef uint16` (only uint16 and uint32
                         were ever instantiated, and this build needs one).
                         IMPORTANT: this de-templating was NOT the fix for that
                         file's crash and is NOT load-bearing - see section 12.

NEW TRANSLATION UNIT - engines/kyra/screen_delta.cpp.  The delta-frame decoders
were SPLIT OUT of screen.cpp (3780 lines) into their own small TU.  De-templating
alone was not enough: the crash walked from pass to pass as the code was reshaped
(GEM_CX_EXTEND_LIFETIME <-> GEM_FG_FIND_BRANCH) and the traceback showed it firing
during screen.cpp's TEMPLATE-INSTANTIATION FLUSH - i.e. an INTERACTION with the
many other instantiations in that file, not a fault in the routine.  The new TU
instantiates no Common containers, and the back-end emits it.  It is built
/NOOPTIMIZE by an explicit rule in DESCRIP.MMS, and being small it is also cheap
to recompile under the emulator.
  BUILD NOTE: screen_delta.cpp is the 358th object.  If it is ever removed,
  Screen::decodeFrameDelta* lose their definitions and the link fails.

------------------------------------------------------------------------------
2. SEVERED TRANSITIVE INCLUDES  (fallout of the hashmap.h change above)
------------------------------------------------------------------------------
Undefining USE_HASHMAP_MEMORY_POOL removed the
  hashmap.h -> memorypool.h -> array.h -> textconsole.h
chain, so error()/warning() stopped being declared.  Explicit
#include "common/textconsole.h" added to:
  backends/timer/default/default-timer.cpp   (was fatal: %CXX-E-UNDECLARED)
  common/installshield_cab.cpp               (was fatal)
  graphics/fonts/ttf.cpp                     (latent - TU is empty without
                                              USE_FREETYPE2, added anyway)
  engines/kyra/staticres.cpp                 (common/debug.h, for debug())

------------------------------------------------------------------------------
3. MISSING POSIX FACILITIES  (__VMS)
------------------------------------------------------------------------------
backends/fs/posix/posix-fs.cpp      <sys/param.h> not included (absent on VMS);
                                    MAXPATHLEN comes from scummsys.h instead.
                                    struct dirent has no BSD d_type field, so
                                    the stat()-based fallback path is forced.

backends/platform/sdl/posix/posix.cpp
                                    displayLogFile() returns false - no fork(),
                                    execlp() or waitpid() on VMS.  Convenience
                                    feature only; never reached in this build.

------------------------------------------------------------------------------
3b. THE FORBIDDEN-SYMBOL LAYER vs THE VMS CRTL HEADERS  (__VMS)
------------------------------------------------------------------------------
common/forbidden.h macro-redefines banned symbols to an intentionally unparsable
token sequence unless the TU first #defines the matching
FORBIDDEN_SYMBOL_EXCEPTION_*.  On VMS this collides with the CRTL headers
themselves: <unistd.h> reaches the DECC$TYPES module of
SYS$COMMON:[SYSLIB]DECC$RTLDEF.TLB, which DECLARES getenv - and that declaration
then fails to parse:
      %CXX-E-EXPDECL, expected a declaration
      at line number 1 in module DECC$TYPES of ...DECC$RTLDEF.TLB
THE ERROR POINTS AT A SYSTEM TEXT LIBRARY, NOT AT YOUR FILE.  The fix is a
missing exception in the TU that included <unistd.h>, so diff the exception block
against a sibling TU that compiles - that is what identified both cases here:

backends/fs/posix/posix-fs-factory.cpp     FORBIDDEN_SYMBOL_EXCEPTION_getenv
backends/fs/chroot/chroot-fs-factory.cpp   added.  They were the only two TUs
                                    including posix-fs.h that lacked it;
                                    posix-fs.cpp and chroot-fs.cpp already had
                                    it and compiled fine.

TRAPS WORTH KNOWING (each cost a round in this port):
  - FORBIDDEN_SYMBOL_EXCEPTION_unistd_h does NOT cover getcwd.  getcwd
    (forbidden.h:303) needs its OWN exception - see 10(b).
  - printf is forbidden too (forbidden.h:60).  Use warning() for a message that
    must appear unconditionally; debug() needs -d on the command line.
  - stdiostream.cpp is exempt wholesale (#define FORBIDDEN_SYMBOL_ALLOW_ALL), so
    the fopen work in 10(a) needed no exception.

------------------------------------------------------------------------------
4. STRUCT LAYOUT  (__VMS) - was a startup ACCVIO
------------------------------------------------------------------------------
backends/platform/sdl/sdl-sys.h     #pragma member_alignment after the SDL
                                    includes.  The VMS SDL 1.2 headers turn
                                    struct packing ON and never turn it off, so
                                    every class declared afterwards - including
                                    OSystem - got a PACKED layout while TUs that
                                    never see SDL got the aligned one.  An ODR
                                    violation on the program's most-used class:
                                    sizeof(OSystem) 28 vs 25, vptr at 24 vs 21.
                                    Straddled vptr reads sent control into the
                                    wrong vtable.  Measured with PROBE_D.CPP.
                                    WARNING: this header is NOT in
                                    $(GLOBAL_HDRS) (adding it invalidates all
                                    358 objects = ~8h rebuild), so editing it
                                    requires a manual MMK CLEAN.

------------------------------------------------------------------------------
5. NO-SOUND CONSEQUENCES  (KYRA_STANDALONE) - runtime hangs, not build errors
------------------------------------------------------------------------------
backends/platform/sdl/sdl.cpp       NullSdlMixerManager instead of
                                    SdlMixerManager - no SDL audio device is
                                    opened.
base/commandLine.cpp                music/sfx/speech/mute default to true.

engines/kyra/sequences_darkmoon.cpp waitForSongNotifier(): fixed 1-second
                                    wall-clock wait replaces the music cue.
                                    Sound::checkTrigger() (sound.h:261) is a
                                    non-overridden "return 0" and every caller
                                    waits for index >= 1, so the upstream loop
                                    condition is PERMANENTLY TRUE - 13 sites in
                                    the EOB2 intro/finale busy-waited (no
                                    delayMillis) until a keypress released them.
                                    Looked like a slow intro; was a HANG.
                                    CONFIRMED FIX - intro now runs correctly.
                                    Not needed elsewhere: isPlaying() /
                                    voiceIsPlaying() return FALSE with no sound,
                                    so those loops fall through harmlessly.

------------------------------------------------------------------------------
6. PERFORMANCE  (KYRA_STANDALONE) - trade-off, not a bug
------------------------------------------------------------------------------
engines/kyra/sequences_lol.cpp      LoL intro pump delayMillis(10) -> 30.
                                    Each iteration ran checkedPageUpdate(8,4)
                                    (screen_v2.cpp), an unconditional 16000-
                                    uint32 diff of the whole 64KB page, at up to
                                    100 Hz = ~1.6M iterations/sec.  Cost is
                                    ALGORITHMIC, not unoptimized code
                                    (screen_v2.cpp builds at LEVEL=3).  Pacing
                                    is unaffected: TIM script and palette fade
                                    are both wall-clock driven and catch up.
                                    STATUS: in, and the intro is acceptable -
                                    but see the SIDE EFFECT note in section 7.
                                    Much of the "slow intro" was really the SDL
                                    timer thread plus the per-call cost of
                                    SDL_GetTicks(), both now gone, so this
                                    delayMillis bump may no longer be needed.
                                    If the intros are ever revisited, try
                                    reverting 30 -> 10 FIRST and re-measure.

------------------------------------------------------------------------------
7. THE INTERMITTENT ACCVIO  (__VMS) - CONFIRMED FIXED
------------------------------------------------------------------------------
Symptom: random %SYSTEM-F-ACCVIO, minutes apart, in EOB1/EOB2/HoF, sometimes
before the game even loaded.  Five dumps.  The game, engine and call site all
varied (runLoop / updateWallOfForceTimers / kyra timer update); the two
innermost frames NEVER did:
      PC=0x4C / 0x0 / 0x20000000 / 0x7ADD80A0   (four different bogus PCs)
      DECC$SHR_EV56                    ?  ?
      sdl_systimer  SDL_GetTicks  +0x54
      sdl           getMillis
Cause: LIBSDL:[SRC.TIMER.UNIX]SDL_systimer.c.  SDL_GetTicks() is a thin
wrapper that calls gettimeofday() (HAVE_CLOCK_GETTIME undefined) and subtracts
a file-static "start" timeval.  +0x54 IS that gettimeofday() call - hence the
unresolvable DECC$SHR frame beneath it.  getMillis() is the hottest call in
the program, so it was the first to trip over the bad transfer.

backends/platform/sdl/sdl.cpp        getMillis() reads the clock DIRECTLY with
                                     gettimeofday() instead of SDL_GetTicks().
                                     Identical semantics (ms since first call,
                                     wrapping at 2^32 like SDL's uint32) minus
                                     SDL's static and the cross-image call.
                                     Same idiom as the sibling VMS ports
                                     (freesci scicore/tools.c, sarien x11.c).
                                     NOT sys$gettim: gettimeofday() is plain C
                                     RTL, no starlet.h, no 64-bit VMS-time
                                     conversion, file stays portable.
                                     CONFIRMED FIX - user reports it resolved.

DO NOT ADD SDL_INIT_TIMER TO SDL_Init.  An earlier revision of this port did,
on the theory that SDL_GetTicks() needs the TIMER subsystem initialised.  That
theory was WRONG and the change was HARMFUL.  This SDL is built with
SDL_TIMER_UNIX and WITHOUT SDL_THREADS_DISABLED, so USE_ITIMER is undefined
and SDL_SYS_TimerInit() takes the threaded branch: it does
SDL_CreateThread(RunTimer), and RunTimer spins "SDL_ThreadedTimerCheck();
SDL_Delay(1)" ~1000x/sec, calling SDL_GetTicks() from that thread while the
main thread does the same.  So the flag SPAWNS A THREAD.
  LESSON: this cost a whole test round.  Removing SdlTimerManager (below) and
  adding that flag CANCELLED OUT - upstream only ever initialises TIMER from
  SdlTimerManager's constructor, so the SDL timer thread was created anyway
  and the crash recurred unchanged, wasting the experiment.  Change ONE thing
  per 8-hour test cycle, and read the LIBRARY SOURCE (which #ifdef branch
  actually compiles) before theorising about a library frame.

backends/platform/sdl/sdl.cpp        Two further changes, made while the timer
                                     thread was still the suspect.  Neither is
                                     the fix; both are correct on their own
                                     merits, so they stay:
                                     (a) initBackend() installs a plain
                                     DefaultTimerManager, not SdlTimerManager,
                                     so no 10ms SDL_AddTimer thread is
                                     started.  Nothing here needs it: the only
                                     installTimerProc() callers are
                                     audio/fmopl.cpp (RealOPL - never
                                     selected; the factory returns
                                     mixer-driven EmulatedOPL),
                                     audio/mpu401.cpp (needs a MidiDriver, and
                                     none is ever built - eobcommon.cpp:386
                                     hardwires SoundAdLibPC, and for
                                     LoK/HoF/MR/LoL detectDevice() falls to
                                     MT_ADLIB because mt32_device/gm_device
                                     default to "null") and
                                     audio/softsynth/mt32.cpp (USE_MT32EMU
                                     undefined).  The slot queue is always
                                     empty.  NOTE: ENABLE_EVENTRECORDER *IS*
                                     defined (config.h:44), so the LIVE branch
                                     is the g_eventRec one - the #else is dead
                                     code.  Editing the #else has no effect.
                                     (b) ~OSystem_SDL() destroys the timer
                                     manager FIRST.  Upstream does it AFTER
                                     _graphicsManager, _eventSource,
                                     _mixerManager etc., so a live timer
                                     callback re-enters a half-destroyed
                                     OSystem_SDL (getMillis, lockMutex) - and
                                     quit() is "delete this; exit(0)", so that
                                     is every exit.
                                     The EXIT HANG (was section 10) is now GONE
                                     too: the game returns to DCL cleanly, no
                                     Ctrl+C.  Which of (a)/(b) did it is not
                                     separable - they shipped together - but
                                     both removed a timer callback that was
                                     live during teardown, so the mechanism is
                                     no mystery.

SIDE EFFECT: play and load are also noticeably FASTER.  Expected, not luck.
getMillis() is called constantly (every runLoop, every TIM script step, every
wait loop) and it now costs one RTL gettimeofday() instead of a cross-image
call into LIBSDL; and with no SDL timer thread there is no longer a second
thread waking ~1000x/sec (SDL_Delay(1) in RunTimer) stealing cycles from an
emulated ES40.  This is the "speeds" work reappearing for free after being
dropped from scope - the intros were partly paying for that thread.

backends/mixer/nullmixer/nullsdl-mixer.cpp
                                     "delete _samplesBuf" -> "delete[]"; the
                                     buffer is new uint8[_samples*4].  Runs on
                                     the exit path.

------------------------------------------------------------------------------
8. STANDALONE LOADER  (KYRA_STANDALONE)
------------------------------------------------------------------------------
base/main.cpp                       autodetectCurrentDir() replaces the launcher
                                    GUI: detects a supported game in the CWD and
                                    makes it the active target (FreeSCI/Sarien
                                    "run from the game data directory" model).
base/commandLine.cpp                Fixed 640x480 windowed output:
                                    gfx_mode=2x, aspect_ratio=true,
                                    fullscreen=false.

------------------------------------------------------------------------------
9. RUNTIME REQUIREMENTS - NOT source changes, but needed to run
------------------------------------------------------------------------------
RUNSETUP.COM   Must be run from the game data directory before KYRA.EXE.  It
               lives on the VMS side, NOT in this source tree - (a) and (b) below
               are its full contents, so it can be retyped from this section.
  (a) DECC$FILENAME_UNIX_REPORT / _UNIX_ONLY / _UNIX_NO_VERSION /
      READDIR_DROPDOTNOTYPE / EFS_CHARSET / ARGV_PARSE_STYLE = ENABLE.
      ScummVM's file layer is POSIX-only and composes paths with '/', so with
      the RTL in VMS mode getcwd() returns DKA0:[GAMES.EOB2] and every child
      path becomes the hybrid DKA0:[GAMES.EOB2]/LEVEL15.INF, which opens
      nothing.  Symptom: "no supported game found in the current directory"
      with NO per-file diagnostics at all.  Deliberately NOT enabled:
      DECC$EFS_CASE_PRESERVE (detector maps are already case-insensitive).
  (b) SET FILE/ATTRIBUTES=(RFM:STMLF,RAT:NONE,EBK:971,FFB:36) KYRA.DAT
      VMS files occupy whole 512-byte blocks and 496676 = 970*512 + 36 is not a
      block multiple, so a block-oriented transfer puts RMS EOF at 497152.
      checkKyraDat() takes size()-16 as the payload, reading the digest out of
      the padding and hashing 476 padding bytes.  The DATA IS INTACT - only the
      byte COUNT was wrong.  Symptom: "missing KYRA.DAT or it got corrupted".
  (c) KYRA foreign command, so options can be passed.  RUN takes no arguments,
      and DCL upcases unquoted ones while ScummVM's option names are lowercase:
          $ KYRA "--gfx-mode=1x"
      Both (a) and (b) are process-scoped / per-transfer and need no rebuild.

------------------------------------------------------------------------------
10. SAVED GAMES NEVER REACHED DISK  (__VMS) - four separate defects
------------------------------------------------------------------------------
Symptom: saving and restoring worked WITHIN a run, but no save file existed
afterwards, for every game.  Every save that appeared to be there was an
original from a DOS archive.  LoL looked worst (its main menu hides "Load"
unless saveFileLoadable(0) succeeds, sequences_lol.cpp:46 / lol.cpp:682) while
LoK/HoF/MR looked healthy only because they write a "New Game" slot 0 at
startup, and EOB looked healthy only because of defect (c) below.

(a) backends/fs/stdiostream.cpp - the file-CREATION defect.
    makeFromPath() created files with a bare fopen(path,"wb").  On VMS that
    yields rfm=var,rat=cr - a RECORD file - not a byte stream: each fwrite()
    becomes a record and the RTL re-inserts terminators on read, so the byte
    offsets saveload.cpp seeks to stop meaning anything.  The CREATE path now
    passes RMS arguments: ctx=stm,rfm=stmlf,rat=none,fop=sup,mbc=32.  Same
    attribute set RUNSETUP.COM must plant on KYRA.DAT by hand (section 9(b)) -
    creating a file needs it just as much as reading one does.  fop=sup matters
    independently: without it VMS makes a NEW VERSION (LOL-CD.000;2, ;3, ...)
    on every save, reads silently take the highest, and saving fails outright
    once the version limit is reached.  The READ path keeps the plain fopen():
    game data files are whatever the transfer made them, and forcing attributes
    on an existing file fails the open.

(b) backends/saves/posix/posix-saves.cpp - savepath was never set AT ALL.
    The POSIX ctor derives it from HOME / XDG_DATA_HOME; VMS has neither, so
    savePath stayed empty, Posix::assureDirectoryExists() failed on stat("") of
    the empty prefix, and registerDefault("savepath",...) was never reached.
    getSavePath() then returned "", and Common::FSNode's String ctor silently
    maps "" to the CWD (common/fs.cpp:42) - so saves did land in the game
    directory, but by accident, via a path the program could not report, and
    every consumer reading ConfMan.get("savepath") directly (debugger.cpp:525,
    saveload_eob.cpp:989) saw an empty string.  The __VMS branch now registers
    getcwd() explicitly and warning()s it at startup, so "where do the saves go"
    is answerable from a run.  NOTE: --savepath is NOT a workaround on VMS - it
    takes a POSIX path, not a VMS filespec.  Needs
    FORBIDDEN_SYMBOL_EXCEPTION_getcwd (the unistd_h exception does not cover
    getcwd), and warning() not printf() (also forbidden in this TU).

(c) backends/platform/sdl/posix/posix.cpp - SCUMMVM.INI was never written.
    getDefaultConfigFileName() took the "return 0" early exit with no HOME/
    XDG_CONFIG_HOME.  Common::String(NULL) is the EMPTY STRING, so
    createConfigWriteStream() built FSNode("") -> the CWD *directory* -> fopen()
    of a directory -> NULL, and ConfigManager::flushToDisk() returns silently on
    a NULL stream (config-manager.cpp:251).  No diagnostic, ever.  Now returns
    _baseConfigName ("scummvm.ini"), relative, i.e. beside the game data.
    This is what made EOB look fine: importOrigSaves (default true,
    eobcommon.cpp:577) could never be flipped to false, so eobcommon.cpp:521
    re-imported the DOS EOBDATA*.SAV files on EVERY launch.

(d) base/main.cpp - a latent bug that (c) was masking, fixed in the same round
    because fixing (c) ACTIVATES it.  autodetectCurrentDir() called
    GUI::addGameToConf() unconditionally, and that function never replaces: if
    the preferred target exists it appends -1, -2, ... (gui/launcher.cpp:903).
    Correct for the launcher (you are *adding* a game), wrong here (this runs on
    every launch).  Once the config actually persisted the target would drift
    lol-cd -> lol-cd-1 -> lol-cd-2, and since the target IS the save filename
    prefix (getSavegameFilename() = target + ".%03d"), each run would look for a
    different set of files and find none of the last run's.  Now reuses the
    existing game domain when one is present, refreshing only "path".

STATUS: CONFIRMED FIXED - user reports saved games now survive exit.  The four
fixes shipped together in one rebuild (stdiostream.obj, posix-saves.obj,
posix.obj, main.obj - four objects, no headers, so no MMK CLEAN), so which one
was individually decisive is NOT separable from this test.  (b) (c) (d) were
each proven from the source before the rebuild - definite defects on definite
code paths, and (c) alone explains the EOB "phantom saves".  (a) was the leading
theory for "the file is not there after exit" rather than a proven one; it is
now known to be either the cause or harmless, but if the record format ever
matters again, confirm it directly:

    $ DIRECTORY/FULL <target>.000;*

"Record format:  Stream_LF" is what (a) produces.  The startup line added by
(b) ("Kyra/VMS: savepath = ...") reports the directory saves actually go to,
which is the first thing to check if this area ever regresses.

LESSON: on VMS, creating a file is not the same operation as opening one.  A
POSIX-only file layer will compile, run, and read fine while silently producing
files of the wrong RECORD FORMAT - and the failure surfaces only after process
rundown, which looks like data loss rather than a bad open.

------------------------------------------------------------------------------
11. OPENVMS SPLASH-SCREEN BANNER  (__VMS)
------------------------------------------------------------------------------
engines/engine.cpp splashScreen() draws an OpenVMS banner directly under the
ScummVM logo, so the build identifies itself as the VMS port on sight.

Two files: engines/vms_logo_data.h (GENERATED, do not hand-edit) and a
#if defined(__VMS) block in engine.cpp.  Nothing outside __VMS changes.

WHY IT IS A BMP AND NOT THE JPEG: USE_JPEG is #undef in config.h:25, so there
is no JPEG decoder in this build at all.  The art is converted at DEVELOPMENT
time into a C array and decoded at runtime by Image::BitmapDecoder - the same
decoder, and the same format, the stock logo_data.h already uses.  No new
objects, no new library dependency.

WHY MS RLE8 AND NOT A RAW BITMAP - THIS IS THE IMPORTANT PART: uncompressed,
300x249 8bpp is a 75778-byte array initializer, about 5100 source lines.  That
is precisely the input class documented under "Files needing /NOOPTIMIZE" in
DESCRIP.TXT as crashing this compiler's back-end in GEM_DF/CSE (staticres.cpp:
nothing but enormous static resource tables).  Storing the bitmap RLE8-
compressed with a 96-colour palette brings it to 39018 bytes / ~2440 lines, so
engine.cpp keeps compiling at the normal $(CXXFLAGS) level and does NOT need a
$(CXXFLAGS_NOOPT) rule.  Do not "simplify" this back to a raw bitmap.

Image::BitmapDecoder constraints the generator must satisfy (image/bmp.cpp:57):
  - starts "BM"; infoSize EXACTLY 40 (Windows v3 only)
  - height must be POSITIVE, i.e. bottom-up rows ("Right-side up bitmaps not
    supported" is the warning for the other case)
  - bitsPerPixel 8, 24 or 32
  - compression is read with readUint32BE() at bmp.cpp:96, but
    createBitmapCodec() matches SWAP_CONSTANT_32(1) (codec.cpp:199), so the two
    swaps cancel: the field is plain little-endian 01 00 00 00.
And from Image::MSRLEDecoder::decode8 (image/codecs/msrle.cpp:51):
  - decoding starts at the LAST row and DECREMENTS, so emit the TOP row first
  - do NOT write an end-of-line marker after the final row: decode8 does y--
    then tests y<0 and warns "Next line is beyond picture bounds".  The stock
    logo_data.h omits it too.  Terminate with end-of-image (00 01) only.

REGENERATION RECIPE (from src/, with Python + Pillow, on the build host - not on
VMS).  Source art: src/vms_logo.jpg, 516x387.
  1. COLOUR-KEY the black surround to the splash orange (0xd4,0x75,0x0b), using
     a luminance ramp between LO=24 and HI=70 rather than a hard threshold -
     a hard key leaves a dark halo of JPEG ringing around the circle.
  2. CROP to the bounding box of everything that is not within L1 distance 12 of
     that orange, so "300 pixels wide" means 300 pixels of artwork.
  3. SCALE to width exactly 300 - the width of logo_data.h's logo, so the two
     stack flush - with LANCZOS.  Height follows the aspect: 249.
  4. MEDIAN-FILTER 3x3, then quantize to 95 adaptive colours with dither=NONE.
     Both steps exist to make RLE runs longer: dithering roughly doubles the
     encoded size for no visible gain at this scale, and the median filter kills
     isolated JPEG speckles that break runs.  Quality cost is invisible.
  5. PIN palette index 95 to EXACTLY (0xd4,0x75,0x0b) and force every pixel that
     tested as orange in step 2 to that index.  Without the pin, quantization
     drifts the background a shade or two off the overlay fill and the banner
     shows up as a faint rectangle.
  6. Emit a 256-entry palette (unused entries black) like the stock logo, and
     RLE8-encode bottom-up per the decode8 rules above.
VERIFY BEFORE COMMITTING, on the build host: re-parse the generated header and
run the bytes through the exact bmp.cpp/msrle.cpp logic, asserting the decoded
index plane is IDENTICAL to the quantized one, that no decoder warning path is
taken, and that palette[95] is the orange verbatim.  This was done for the
current file: 300x249, 96 colours, 39018 bytes, pixel-exact, zero warnings.
Also compile the header standalone (g++ with a byte typedef and ARRAYSIZE) to
catch a malformed initializer before spending a VMS compile on it.

REBUILD: one object plus the link.  vms_logo_data.h is included by engine.cpp
alone and is NOT in $(GLOBAL_HDRS), so nothing else sees it - and because
DESCRIP.MMS is not a dependency of any target, MMK will not notice the new
header on its own:
    $ DELETE [.engines]engine.obj;*
    $ MMK /IGNORE=WARNING

------------------------------------------------------------------------------
12. METHOD - WHAT ACTUALLY FOUND THESE BUGS
------------------------------------------------------------------------------
Kept because an 8-hour rebuild makes a wasted test round expensive, and every
lesson below was paid for with at least one.

THE VECTORRENDERERSPEC SAGA - SEVEN WRONG DIAGNOSES, ALL INSIDE THE FILE.
graphics/VectorRendererSpec.cpp crashed with %GEM-F-ASSERTION.  The cause was the
/NOOPTIMIZE flag the file was already built with - a flag added at the start as a
"harmless precaution" and therefore never questioned, because it was filed as a
precaution rather than as a change.  Removing it fixed the file on the first try,
with NO source change.  The seven attempts were: (1) /NOOPTIMIZE itself - the
actual cause, unexamined; (2) DISABLE_FANCY_THEMES; (3) colorFill's Duff's
device; (4) drawString temporaries; (5) de-templating the whole class;
(6) guarding out fp_sqroot; (7) #undef DISABLE_FANCY_THEMES.
  * ALL SEVEN TRACEBACKS WERE BIT-IDENTICAL.  That is the signal that was missed:
    you cannot rewrite every routine in a module and get a byte-for-byte
    identical codegen failure unless the real variable is OUTSIDE the source.
    RULE: when successive source reshapes leave a compiler traceback IDENTICAL,
    STOP EDITING SOURCE and look at what stayed CONSTANT - here, the flags.
  * DISABLE_FANCY_THEMES was pushed BOTH ways (defined round 2, #undef round 7)
    and the build failed identically both times, which alone rules it out.
  * /NOOPTIMIZE IS NOT FREE AND CAN CAUSE CRASHES OF ITS OWN.  GEM_CX is the code
    EXPANDER and runs at every optimization level, so /NOOPTIMIZE can never fix a
    GEM_CX crash - but it can cause one: unoptimized, nothing is inlined away and
    no live range is split, so a routine's expanded body and live-variable set
    hit their MAXIMUM exactly when the per-routine expander walks them.
    OPTIMIZATION SHRINKS THE INPUT.  Never add /NOOPTIMIZE "as a precaution" or
    "because the file is big".  Read the failing PASS NAME first; the taxonomy is
    in DESCRIP.TXT (GEM_DF/CSE -> /NOOPTIMIZE may help; GEM_CX_* -> it cannot).
  * The de-templating (5) and the dropped `register`s (6) were left in place, but
    they are NOT load-bearing and their comments say so.  A disproven fix that
    stays in the tree must be labelled as disproven, or the next person reads it
    as established practice - which is exactly how the `register` theory spread
    to common/math.h.

OTHER LESSONS, each already stated in situ:
  - ASK WHEN IT LAST WORKED, THEN VERIFY IT.  A file that had never been compiled
    was misread as a regression and cost 5 extra rounds.
  - READ THE DISASSEMBLY for a suspected miscompile.  /MACHINE_CODE settled the
    memory.h infinite loop after runtime evidence could not separate 5 theories.
  - READ THE LIBRARY SOURCE when a crash bottoms out in a library, including
    WHICH #ifdef actually compiles.  Guessing spawned an SDL timer thread and
    wasted a whole cycle (section 7).
  - CHANGE ONE THING PER TEST CYCLE.  Two changes in section 7 cancelled out.
  - GOOD DATA BUT A BAD CHECKSUM means audit the byte COUNT, not the parser
    (section 9(b)).
  - KYRA.MAP always exists - the link rule passes /MAP/FULL/CROSS_REFERENCE
    unconditionally - so map analysis costs NO rebuild.  See section 14 for the
    Alpha procedure-descriptor gotcha that made a real frame look fabricated.

------------------------------------------------------------------------------
13. SIERRA ENGINES - AGI + SCI + SCI32  (build structure, not source patches)
------------------------------------------------------------------------------
This section is the one that is NOT a list of source workarounds.  Adding the
Sierra engines needed almost no source change at all: 123 upstream sources were
copied in verbatim (engines/sci, 82 files; engines/agi, 41 files) and ONE
upstream header was copied that the Kyra-only tree had never needed
(image/codecs/truemotion1data.h).  Everything else is DESCRIP.MMS and one
hand-maintained header.

(a) ONE HEADER IS HAND-MAINTAINED - engines/plugins_table.h.
    Upstream generates it with configure; this tree has no configure step, so it
    is edited by hand, and it is the ONE place that decides which engine goes in
    which image.  It is included exactly once (base/plugins.cpp, inside
    PluginManager::init()), and LINK_PLUGIN(X) expands to a reference to
    g_X_getObject / g_X_type - a HARD undefined symbol.  That reference is what
    makes the linker extract engine X from its .OLB.  No reference => the
    engine's library is searched and nothing is taken from it.
    Every upstream engine except KYRA / AGI / SCI was deleted from the list:
    their sources are not in this tree, so a stray LINK_PLUGIN would be a link
    error rather than a silently-absent engine.

(b) base/plugins.cpp IS COMPILED TWICE.  It is the ONLY source file that is.
        [.base]plugins.obj         no extra define          -> KYRA.EXE
        [.base]plugins_sierra.obj  STANDALONE_TARGET_SIERRA -> SIERRA.EXE
    Both compiles see the SAME $(DEFS), so ENABLE_KYRA, ENABLE_AGI and
    ENABLE_SCI are all defined in both.  That is deliberate: the ENABLE_*
    macros also gate whether each engine's own sources compile at all, so
    varying them per image would mean compiling large parts of the tree twice -
    ~8 hours per pass.  KEEP THE PER-IMAGE CHOICE IN plugins_table.h.
    plugins_sierra.obj needs an EXPLICIT rule in DESCRIP.MMS (there is no
    plugins_sierra.cpp) using $(CXXFLAGS_SIERRA), which is a COMPLETE copy of
    $(CXXFLAGS) with the one extra define - a second /DEFINE= on a CXX command
    line REPLACES the first rather than merging with it.

(c) FOUR OBJECT LIBRARIES, NOT ONE - and the reason is the LIBRARIAN, not C++.
    An OpenVMS .OLB keys modules by FILE BASENAME and IGNORES the directory.
    22 basenames collide across kyra / sci / agi / the shared code (console,
    detection, graphics, saveload, script, sound, text, view, ...), and the
    second insert would silently replace the first.  So:
        SCUMM_MAIN.OLB  245 shared modules
        KYRA_ENG.OLB    109
        SCI_ENG.OLB      82
        AGI_ENG.OLB      41
    Each engine library has ZERO internal collisions.  SCUMM_MAIN has exactly
    two (common+gui error, audio/decoders+common quicktime); those two objects
    stay on the LINK command line, as they always did.  sci and agi collide with
    each other on six names but live in different libraries, and both images
    that use them list them separately, so nothing is lost.
    EVERY LIBRARY IS LISTED TWICE ON EACH LINK LINE.  The VMS linker searches a
    library only when it reaches it and will not go back for a reference raised
    by a later library; two passes closes the cycle.

(d) THREE SHARED OBJECTS CHANGE CONTENT because ENABLE_SCI32 is now defined.
    All three were previously compiled COMPLETELY EMPTY - each is wrapped in a
    guard of the form "#if defined(ENABLE_GOB) || defined(ENABLE_SCI32) || ...":
        image/codecs/truemotion1.cpp   (needs the copied truemotion1data.h)
        image/codecs/codec.cpp         (its DUCK/duck FourCC cases are
                                        #ifdef IMAGE_CODECS_TRUEMOTION1_H)
        video/coktel_decoder.cpp       (2860 lines - expect a long compile;
                                        used by sci/engine/kvideo.cpp and
                                        sci/console.cpp for AdvancedVMDDecoder)
    Together with [.base]PLUGINS.OBJ (plugins_table.h changed) and
    [.base]MAIN.OBJ (cosmetic: autodetectCurrentDir()'s three messages said
    "Kyra standalone" and now say "standalone loader", because the same code
    serves both images) these are the ONLY existing objects that must be deleted
    by hand when upgrading a Kyra-only build tree.  KYRA_OBJS.OLB, KYRA.EXE and KYRA.MAP also go, since
    the library layout changed.  DO NOT MMK CLEAN.
    graphics/sjis.h has a similar guard ("!(ENABLE_KYRA || ENABLE_SCI || ...)")
    but ENABLE_KYRA already satisfied it, so sjis.obj is UNCHANGED - do not
    delete it.

(e) NO SOURCE PATCH WAS NEEDED FOR THE NO-SOUND BUILD.  Both engines construct
    their music/MIDI objects unconditionally; MidiDriver::createMidi() resolves
    to the null driver (audio/null.cpp, LINK_PLUGIN(NULL), which base/plugins.cpp
    links in for every image) and writes into a mixer that never opens a device.
    Section 5's no-sound consequences are Kyra-specific and were not repeated.

(f) SCI32 IS EXPERIMENTAL UPSTREAM, and is compiled in anyway on purpose.
    1.8.1's configure.engine says `add_engine sci32 "SCI32 games" no` and every
    SCI32 detection entry carries ADGF_UNSTABLE, so starting an SCI2/2.1/3 game
    shows ScummVM's own "not supported yet / may be unstable" dialog
    (Engine::warnUserAboutUnsupportedGame, engines/engine.cpp, reached from
    engines/advancedDetector.cpp).  THAT DIALOG IS UPSTREAM BEHAVIOUR AND IS
    CORRECT - do not chase it as a port defect.

(g) VALIDATION STATUS - be honest about this.  All 481 sources compile clean
    with g++ -fsyntax-only under the exact final define set, SDL headers
    supplied from /opt/CLAUDE/OPENVMS/sdl through a symlink farm that maps that
    tree's lowercase names onto the capitalised ones ScummVM includes (19 files
    reach SDL.h via gui/EventRecorder.h -> backends/mixer/sdl/sdl-mixer.h and
    cannot be syntax-checked without it).  NOTHING in engines/sci or engines/agi
    has met the HP C++ back-end yet.  The crash taxonomy in DESCRIP.TXT was
    derived entirely from the Kyra sources; expect some sci/agi files to need
    the same per-file flags.  READ THE GEM_* PASS NAME FIRST (section 12) - do
    not start by adding /NOOPTIMIZE.

------------------------------------------------------------------------------
14. OPEN ITEMS
------------------------------------------------------------------------------
- RESOLVED, kept here as method: the intermittent ACCVIO and the exit hang are
  both FIXED - see section 7.  Reading KYRA.MAP was what cracked it, and no
  rebuild was needed: the `kyra.exe :` link rule in DESCRIP.MMS ALREADY passes
  /MAP=kyra.map/FULL/CROSS_REFERENCE unconditionally, so KYRA.MAP always
  exists.  (The empty $(LINKFLAGS) macro is redundant for this - do not relink
  for it.)  Named, not line-numbered, because the comment split moved the lines.
    $ SEARCH KYRA.MAP "<module>"              -> psect contributions + symbols
    $ SEARCH/WINDOW=(30,10) KYRA.MAP "<addr>" -> psect NAME and neighbours
  ALPHA MAP GOTCHA that cost a wrong conclusion: in the symbol cross-reference
  a routine's value is the address of its PROCEDURE DESCRIPTOR, not its entry
  point.  sdl_systimer showed TWO contributions - 416 bytes of descriptors at
  0x000F2CE0 and 1012 bytes of CODE at 0x0082A120.  Dividing the descriptor
  psect by the routine count "proves" 32-byte routines and makes a genuine
  rel PC look impossible.  Check the psect ATTRIBUTES (the code one is
  PIC,CON,REL,LCL,SHR,EXE,NOWRT) before concluding a frame is fabricated.
  The "line" column IS meaningless for library modules compiled without debug
  data (it reported 11109, 42994 for a 700-line file) - ignore it, but do not
  extend that distrust to the rel PC.
- engines/kyra/staticres.cpp carries an UNBUILT, UNTESTED __VMS fallback
  (recoverKyraDatPayloadSize) that recovers KYRA.DAT's true length from the PAK
  index.  Redundant with 9(b).  Either build and test it or revert it - do not
  leave it half-applied.
- createLogFile() (backends/platform/sdl/posix/posix.cpp) is the LAST remaining
  user of the getenv("HOME") + '/' concatenation layer described in 9(a); the
  config and save paths beside it are now fixed (section 10).  It is only
  reached with --debuglevel, so it is harmless, but it will produce the same
  empty-path-becomes-a-directory result if it ever is.
- engines/vms_logo_data.h is not in $(GLOBAL_HDRS) - see the warning in section
  4 - but deliberately and harmlessly: only engine.cpp includes it (section 11).
  sdl-sys.h WAS in this list and is now IN $(GLOBAL_HDRS), added 2026-08-28 in
  the same edit that raised $(OPT), since that forced a full rebuild anyway.
- UNTESTED ON HARDWARE: the splash banner (section 11) has been verified only by
  emulating bmp.cpp/msrle.cpp on the build host and by a standalone g++ compile.
  It has NOT yet been through a CXX compile or an actual run.  First things to
  check on the next build are in REBUILD_LOGO.COM.
- The LoL delayMillis(30) in section 6 is probably no longer needed now that the
  SDL timer thread is gone.  Try reverting 30 -> 10 and re-measure.

------------------------------------------------------------------------------
15. LEVEL=4 FALLOUT - things the higher $(OPT) surfaced
------------------------------------------------------------------------------
Raising $(OPT) to LEVEL=4 (header, "Opt") does not only change code generation:
LEVEL=4 adds inline expansion over LEVEL=3, and the optimizer's flow analysis
then sees ACROSS the inlined bodies.  So it emits diagnostics the same source
never triggered at LEVEL=3.  Log them here as they appear.

FIRST RULE FOR ALL OF THEM: %CXX-W-* is a WARNING.  The object file IS written.
MMK aborts the target anyway, because to MMK any non-success status is a failure
- so BUILD WITH  MMK /IGNORE=WARNING.  That is not a workaround for this section,
it is the documented invocation for this tree (see the header, "MMK").  Do NOT
reach for /NOOPTIMIZE or add the warning to the tree-wide
/WARNINGS=(DISABLE=(...)) list because a build stopped.

(a) audio/midiparser_qt.cpp - %CXX-W-UNINIT "variable info.start ... is fetched,
    not initialized", reported at common/list_intern.h:43 and naming lines 121,
    187 and 362 of midiparser_qt.cpp.  FIXED IN SOURCE.

    Upstream fills EventInfo (audio/midiparser.h) field by field and leaves
    ::start unset, because MidiParser_QT never reads it - start is only
    meaningful for the delta-based parsers (SMF, XMIDI).  _queuedEvents.push(info)
    then copies the WHOLE struct, so Common::List's
        Node(const T &x) : _data(x) {}
    fetches the indeterminate members.  That constructor is what LEVEL=4 inlines,
    which is why the diagnostic points into list_intern.h while blaming a
    declaration in the .cpp.  It is formally UB but harmless in practice: nothing
    in the QuickTime path reads start, and on the two paths that also leave
    length / ext.data unset the event types involved (0xFF/0x2F end-of-track,
    0xC0/0xB0/0xE0) never read those either.

    Fix: a file-local  static void clearEventInfo(EventInfo &)  that memsets the
    struct, called at all FOUR declaration sites (the compiler named three; the
    fourth, handleControllerEvent, has the same defect).  Two deliberate choices:
      * memset, not field-by-field.  The basic/ext union is 16 bytes wide because
        ext.data is 8-byte aligned, while basic is 2, so setting every NAMED
        member still leaves padding indeterminate and the warning can return.
        EventInfo is a POD - no constructor, no virtuals - so memset is defined.
      * The helper is LOCAL TO THE .CPP, and audio/midiparser.h was NOT touched
        (e.g. by giving EventInfo a constructor).  midiparser.h is not in
        $(GLOBAL_HDRS), so editing it would silently invalidate every object
        that includes it with MMK unable to see it - the exact trap in section 4.
        This way exactly ONE object goes stale:
            $ DELETE [.AUDIO]MIDIPARSER_QT.OBJ;*
      No behaviour change, and it applies cleanly to a stock 1.8.1 tree too.

==============================================================================

------------------------------------------------------------------------------
16. HP C++ V7.3 ALPHA MANGLES TOP-LEVEL const ON VALUE PARAMETERS
------------------------------------------------------------------------------
This is a genuine COMPILER BUG, not a source defect, and it produced the first
SIERRA.EXE link failure.  Read this before "fixing" any undefined C++ symbol
that you can plainly see defined in the tree.

THE RULE THE COMPILER GETS WRONG.  Per C++ [dcl.fct]/5, a top-level cv-qualifier
on a by-value parameter is DISCARDED when forming the function's type.  These
two declare the same function, and every conforming compiler gives both the same
mangled name:

    void f(const int x);        /* in the header    */
    void f(int x) { ... }       /* in the .cpp      */

HP C++ V7.3-009 on Alpha matches them correctly - it does NOT complain that the
definition has no declaration in the class - but it then MANGLES THE NAME FROM
THE WRITTEN FORM.  So the definition exports  f(int)  while every caller, which
only ever sees the header, imports  f(const int).  The two never meet.

HOW IT PRESENTS.  As undefined symbols at link time for functions that are
obviously present, with the const visible in the linker's own demangled text:

    %LINK-I-UDFSYM, void Sci::GfxPalette32::kernelPalVaryMergeStart(const int)
    %LINK-I-UDFSYM, Sci::Script *Sci::SegManager::getScript(unsigned short)

Note the asymmetry, which is the tell: the first has a const the definition
lacked (header said const), the second lacks a const the definition had (header
said plain).  In both cases the name that is MISSING is the one the CALLERS
built, i.e. the header's spelling.

*** AND THE IMAGE STILL GETS WRITTEN. ***  VMS LINK treats undefined symbols as
%LINK-W-, a WARNING, and produces the .EXE anyway with every unresolved call
site pointing at address 0.  Combined with the MMK/IGNORE=WARNING this tree
requires (section 15), the build prints "build complete" and the image dies at
the first such call:

    %SYSTEM-F-ACCVIO, ... virtual address=0000000000000000, PC=0000000000000000
     SIERRA  state  initGlobals   ...        <- initGlobals calls getScript()

A PC of 0 with a virtual address of 0 in a frame whose caller is obvious is
almost always this, not a null pointer in the game logic.  ALWAYS re-read the
link output before running a new image; "%LINK-W-NUDFSYMS, n undefined symbols"
means the image is broken no matter what MMK said afterwards.

WHY ONLY 7 OF THE 15 MISMATCHES IN THIS TREE FAILED.  A mismatch only breaks the
link if the function is called BY NAME FROM ANOTHER OBJECT.  It is invisible when
  * the function is VIRTUAL - the vtable slot is filled in the same object that
    defines it, so the mangled name never has to match across objects.  This is
    why the ~20 readBuffer(int16 *, const int) overrides in audio/ have always
    linked, and why graphics/fonts/bdf.cpp's drawChar was harmless; and
  * every call is inside the defining .cpp - the compiler binds those directly.
    That covers GfxPalette32::clearCycleMap / setCycleMap / getCycler,
    MaxTrax::controlCh / noteOn, Tfmx::trackRun / noteCommand and
    GfxMgr::drawCharacterOnDisplay, all of which were latent land mines.

THE FIX, AND WHICH SIDE TO CHANGE.  Make the two spellings agree.  ALWAYS EDIT
THE .CPP TO MATCH THE HEADER, never the other way round: changing a header
re-stales every object that includes it (and for anything reachable from
$(GLOBAL_HDRS) that is most of the tree), whereas changing the definition makes
exactly one object stale.  Five of the sixteen edits below therefore ADD a const
that upstream omitted and eleven REMOVE one upstream had - the direction is
whatever the header already says.  All sixteen are semantically no-ops.

    engines/sci/engine/seg_manager.cpp   getScript, getScriptIfLoaded     -const
    engines/sci/graphics/palette32.cpp   kernelPalVaryMergeTarget         +const
                                         kernelPalVarySetTarget           +const
                                         kernelPalVarySetStart            +const
                                         kernelPalVaryMergeStart          +const
                                         setFade                          +const
                                         clearCycleMap, setCycleMap       -const
                                         getCycler                        -const
    engines/agi/graphics.cpp             drawCharacterOnDisplay           -const
    audio/mods/maxtrax.cpp               controlCh, noteOn                -const
    audio/mods/tfmx.cpp                  trackRun, noteCommand            -const
    graphics/fonts/bdf.cpp               drawChar                         -const

Six objects go stale; no header changed, so nothing else does:

    $ DELETE/NOCONFIRM/NOLOG [.ENGINES.SCI.ENGINE]SEG_MANAGER.OBJ;*
    $ DELETE/NOCONFIRM/NOLOG [.ENGINES.SCI.GRAPHICS]PALETTE32.OBJ;*
    $ DELETE/NOCONFIRM/NOLOG [.ENGINES.AGI]GRAPHICS.OBJ;*
    $ DELETE/NOCONFIRM/NOLOG [.AUDIO.MODS]MAXTRAX.OBJ;*
    $ DELETE/NOCONFIRM/NOLOG [.AUDIO.MODS]TFMX.OBJ;*
    $ DELETE/NOCONFIRM/NOLOG [.GRAPHICS.FONTS]BDF.OBJ;*
    $ DELETE/NOCONFIRM/NOLOG SIERRA.EXE;*
    $ DELETE/NOCONFIRM/NOLOG KYRA.EXE;*

(KYRA.EXE goes too because bdf/maxtrax/tfmx are in SCUMM_MAIN.OLB, which both
images link.  Its behaviour does not change - those three were the harmless
cases - but relinking keeps the two images consistent.)

HOW TO FIND THE REST.  The %LINK-W-WRNERS lines are a MAP OF THIS BUG.  The
first SIERRA link reported "compilation warnings" in exactly bdf, seg_manager,
palette32 and graphics(AGI) - four of the five - i.e. the compiler flags these
files even when the mismatch happens to be link-invisible.  (The fifth, dbopl,
is unrelated.)  So: if a module shows up in %LINK-W-WRNERS and you cannot
account for the warning, check its definitions against its header for exactly
this.  The mechanical check, run on the host, is to compare each
"Class::method(params)" in a .cpp with the declaration of that method inside
"class Class" in the paired .h, normalising away top-level const, and report
where the raw spellings differ.  The tree is CLEAN as of 2026-08-28: zero
same-class mismatches remain in all 481 sources.

Do NOT try to work around this with a compiler switch or by adding the symbol to
/WARNINGS=(DISABLE=(...)); the mangling is wrong, not the diagnostic.

==============================================================================

------------------------------------------------------------------------------
17. NO THREADS MEANS NOTHING DRIVES THE TIMER MANAGER OR THE MIXER
------------------------------------------------------------------------------
SYMPTOM.  SIERRA.EXE runs.  Quest for Glory I VGA is playable start to finish.
Every other Sierra game either sits on the Sierra logo screen forever or goes to
a black screen just after it.  No crash, no error, no traceback - the process is
alive, the window repaints, input is accepted, the game just never advances.

ROOT CAUSE.  Two subsystems that upstream ScummVM drives from BACKGROUND THREADS
are not driven at all in this build, because this port deliberately removed both
threads (see the long notes in backends/platform/sdl/sdl.cpp initBackend() and
initSDL() - every one of those threads was a source of intermittent ACCVIOs on
this platform, and removing them is why the Kyra build became stable):

  1. Common::TimerManager.  DefaultTimerManager::handler() is the function that
     fires installTimerProc() callbacks.  Upstream it is called from an
     SDL_AddTimer(10 ms) callback running on the SDL timer thread.  With no
     thread, NOTHING calls handler(), so installed timer procs never fire.

  2. Audio::Mixer.  MixerImpl::mixCallback() is normally called by SDL's audio
     callback thread.  This build uses NullSdlMixerManager, which opens no audio
     device at all; its own update() pump exists only for the event recorder and
     is called from EventRecorder::updateSubsystems(), i.e. never here.  So
     mixCallback() was never called even once.

Neither mattered for Kyra, and the sdl.cpp comment said so explicitly - it
enumerated every installTimerProc() caller in the tree and concluded "the
TimerSlot queue is ALWAYS empty".  That conclusion was correct FOR THE ENGINES
BUILT AT THE TIME and is exactly the kind of reasoning that rots.  Adding SCI
added a caller.  That comment is now corrected in place.

WHAT SCI NEEDS THEM FOR.

  Palette fades.  engines/sci/graphics/palette.cpp:824 does

      g_sci->getTimerManager()->installTimerProc(&palVaryCallback, ...)

  and palVaryCallback is the ONLY thing that increments GfxPalette::_palVarySignal.
  GfxAnimate::kernelAnimate (animate.cpp:592) calls palVaryUpdate(), which does
  nothing while _palVarySignal is 0, so _palVaryStep stays at the 1 that
  kernelPalVaryInit() set and kernelPalVaryGetCurrentStep() returns 1 forever.
  A script that fades a picture in and waits for the step to reach its stop value
  waits for ever - and the inbetween palette it is stuck on is 1/64 of the way
  from the origin palette, which if the origin was black IS a black screen.  That
  is both reported symptoms, from one cause: hung on the logo, or black just
  after it.

  Sound cues.  SciMusic::onTimer() (music.cpp:164) is what advances every
  MidiParser_SCI, and it is reached only through the driver's timer callback set
  at music.cpp:127.  For MT_ADLIB - which is what detectDevice() picks here,
  music_driver defaulting to "auto" - that callback is invoked by
  MidiDriver_AdLib::onTimer, which Audio::EmulatedOPL calls as a side effect of
  the MIXER draining its readBuffer().  Frozen mixer, no MIDI ticks, no sound
  ever finishes, no cue is ever posted, and every intro that advances on a cue
  hangs.  Same for speech: SCI's kDoAudio polls Mixer::getSoundElapsedTime(),
  which is computed from samples consumed, so digital audio never "ends" either.

  Note that muting is irrelevant to all of this - KYRA_STANDALONE sets mute=true
  (commandLine.cpp) and that only zeroes output volume.  The mixer still has to
  RUN, because it is being used as a clock, not as an audio device.

WHY QFG1VGA IS THE EXCEPTION.  It reaches its first interactive screen without
waiting on either mechanism.  kernelPalVaryInit() has a no-timer path - "if no
ticks are given, jump directly to destination", palette.cpp:846 - and a game that
only ever uses that path, or does its logo with a plain DrawPic and a transition,
never notices.  QFG1VGA working was luck, not evidence that the port was sound.

THE FIX.  Pump both from the main thread.  Neither needs a thread back:
DefaultTimerManager::handler() is time-based and CATCHES UP (it loops while a
slot is overdue), and the new NullSdlMixerManager::updateRealtime() generates
exactly the number of samples wall-clock time says are due.  So the pump can be
called at any irregular rate and the resulting timing is still right.

  backends/mixer/nullmixer/nullsdl-mixer.h
  backends/mixer/nullmixer/nullsdl-mixer.cpp
      NEW updateRealtime(uint32 nowMillis).  Upstream update() generates a fixed
      tiny amount of audio per call, which is useless as a clock.  updateRealtime()
      computes bytes = outputRate * elapsedMillis / 1000 * 4 and feeds it to
      callbackHandler() in buffer-sized chunks.  It ignores intervals under 10 ms
      (not worth a mix pass) and CAPS the backlog at 250 ms, so a long stall -
      loading a room, or startup - does not fire hundreds of MIDI ticks in one go
      and make the music lurch.  Dropping that excess just means silence was
      missed, which is correct for a build with no audio output.

  backends/platform/sdl/sdl.h
  backends/platform/sdl/sdl.cpp
      NEW OSystem_SDL::vmsRunAsyncSubsystems(), under #if defined(__VMS).  Calls
      DefaultTimerManager::handler() and NullSdlMixerManager::updateRealtime(),
      both reached through getTimerManager()/getMixerManager() so that it always
      pumps the objects the engine is actually using (under ENABLE_EVENTRECORDER
      those go through g_eventRec).  Guarded by _vmsPumping against re-entry,
      since mixCallback() runs engine sound code.

      Hooked into BOTH updateScreen() (new override) and delayMillis().  Both are
      needed.  delayMillis() covers every wait - SciEngine::sleep() spins on
      delayMillis(10) and the speed throttler goes through it - but on hardware
      too slow to hit the target frame rate the throttler never sleeps at all, so
      updateScreen() is what guarantees the pump still runs.  Kyra is unaffected:
      the pump only makes callbacks fire that Kyra never installs.

REBUILD.  BOTH HEADERS CHANGED CLASS LAYOUT and DESCRIP.MMS has no per-file
header dependencies - it tracks $(GLOBAL_HDRS) only - so MMK will NOT rebuild the
includers.  UPGRADE_SIERRA.COM deletes all nineteen affected objects; the list is
every object that includes sdl.h or nullsdl-mixer.h, directly or through
gui/EventRecorder.h.  Both images must be relinked: all nineteen live in
SCUMM_MAIN.OLB.

One of the nineteen is easy to talk yourself out of.  POSIX.OBJ must be rebuilt
even though posix.cpp did not change, because it emits the OSystem_POSIX vtable
and updateScreen() is now overridden one level up in OSystem_SDL.  Keep the old
POSIX.OBJ and that vtable slot still points at ModularBackend::updateScreen; the
pump is never called and the fix appears not to work.

STILL EXPECTED TO FAIL: SCI32.  Independently of any of the above, the SCI2 and
SCI2.1 games (GK1, Shivers, PQ4, SQ6, Torin, Phantasmagoria, ...) are marked
ADGF_UNSTABLE in ScummVM 1.8.1 - 89 entries in engines/sci/detection_tables.h.
Upstream 1.8.1 could not complete them on any platform; SCI32 graphics and video
were still being written and did not land until 2.0/2.1.  If an SCI32 game shows
a black screen after this fix, that is upstream, not the port, and chasing it
here means reimplementing two years of upstream SCI32 work.  The games this fix
is expected to make playable are the SCI0/SCI01/SCI1/SCI1.1 and AGI ones.

==============================================================================
