From e313a144805871453779ad51d0b6a70ea4604aa8 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 28 Jul 2026 22:10:01 -0400 Subject: [PATCH] Add physical window size overrides --- docs/engine-re.md | 6 +- docs/phase-a-slice-plan.md | 24 +++++++ docs/platform-portability.md | 2 +- docs/remake-architecture-and-roadmap.md | 7 +- docs/tools-reference.md | 5 ++ .../Age.Engine.Tests/Age.Engine.Tests.csproj | 1 + .../WindowLaunchOptionsTests.cs | 64 +++++++++++++++++++ godot/Main.cs | 30 +++++++-- godot/WindowLaunchOptions.cs | 52 +++++++++++++++ 9 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 engine/Age.Engine.Tests/WindowLaunchOptionsTests.cs create mode 100644 godot/WindowLaunchOptions.cs diff --git a/docs/engine-re.md b/docs/engine-re.md index 7ea357c..7aeff8b 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -201,7 +201,7 @@ The meaningful consumer catalog is: | Setting group | Native Himegari effect | Classification | |---|---|---| | `CREATEOBJECT`, `DRAWMODE` | `CreateObject=2` selects only the retained D3D/object backend; `DrawMode=1` selects its matching draw/text path. Startup verifies that bit `1 << DrawMode` exists in the backend mask. | Active renderer selection | -| `SCREENX`, `SCREENY` | Replaces the generic `640x480` fallback with the `800x600` logical canvas before window/surface creation. The port now parses and applies these dimensions to its logical canvas and default windowed client size. | Active core profile; implemented | +| `SCREENX`, `SCREENY` | Replaces the generic `640x480` fallback with the `800x600` logical canvas before window/surface creation. The port parses and applies these dimensions to its logical canvas and default windowed client size; optional host boot arguments can vary only the physical client. | Active core profile; implemented | | `FONT`, `ENABLEANTIFONT`, `ANTIFONTVERSION` | Seeds MS Mincho, enables `message:UseAntiFont`, selects the grayscale glyph-outline path, and records antialias version 3. | Active text profile | | `DEPENDMOVIESOUND` | Supplies the default movie-audio dependency/routing policy when movie opcode flags do not force another route. | Active media policy | | `FULLSCREENBIT`, `ALWAYSBACKUPSURFACE` | These are read only inside the legacy `CreateObject & 1` DirectDraw branch of `engine_initialize_graphics_from_settings@0x406480`. Himegari selects only bit 2, so its 32-bit/zero values do not drive the active renderer. | Valid compatibility settings, inert for this profile | @@ -226,7 +226,9 @@ This is intentionally a native-behavior catalog, not a mandate to reproduce ever `Sys4AssetCatalog` now parses the ordered trailer once, retaining unknown pairs for diagnostics, and projects `SCREENX`/`SCREENY` into one validated engine-owned logical canvas. Godot uses that value for its content-scale base, default windowed client, backbuffer/compositor bounds, primary surface, movie/layout fallbacks, and input -coordinates. AGE's independent `640x480` defaults apply for missing or invalid dimensions. The port still +coordinates. `--window-width`/`--window-height` may vary that client independently while the logical canvas +and all AGE coordinates remain unchanged. AGE's independent `640x480` defaults apply for missing or invalid +dimensions. The port still preserves requested Mincho/Gothic faces, save version 3.10, and movie/audio behavior through their existing paths; those keys have not yet been migrated to the parsed registry. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 6e6c10a..06b89c2 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3545,6 +3545,30 @@ The Godot C# project builds with zero warnings, and the threaded headless selfte windowed startup directly requests the same validated dimensions. Physical window overrides remain the second slice described above. +### Physical-window override result (implemented 2026-07-28) + +The second slice adds `--window-width ` and `--window-height ` as presentation-only +Godot user arguments. Each omitted dimension independently defaults to the selected SYS4INI logical +canvas; specifying only width or height therefore changes only that physical client dimension. Repeated +options use the last value. Present values must be invariant-culture integers in `1..16384`; missing, +nonnumeric, nonpositive, and oversized values stop startup with a parameter-specific diagnostic. + +`WindowLaunchOptions` is resolved immediately after the logical canvas and before any presentation +allocation. In ordinary windowed mode its result sets `Window.Size`; fullscreen, maximized, or minimized +launch modes retain their display-owned physical size. In every mode `ContentScaleSize`, backbuffers, +primary/offscreen surfaces, ADV layouts, and VM/input coordinates remain tied to the unchanged logical +canvas under Godot's `Viewport`/`Keep` aspect policy. The startup log distinguishes the logical canvas, +requested physical window, request source, actual backend size, and current window mode. The local +launchers remain intentionally narrow conveniences; pass these user arguments directly after Godot's +`--` separator. + +Eleven focused cases cover defaults, both overrides, independent partial overrides, duplicates, missing +values, malformed/nonpositive values, and the upper bound. The complete engine suite passes 454/454 and +the Godot project builds with zero warnings. Headless selftests pass both the default `800x600` request +and an overridden `1200x700` request while continuing to report `logical-canvas=800x600`; invalid zero +width exits during startup with the expected diagnostic. Headless's minimized `64x64` backend remains +non-authoritative for visible client geometry. + ## ADV requested-face selection implemented (2026-07-28) Opcode `0x1a5` is no longer a presentation no-op. The VM retains its requested face in diff --git a/docs/platform-portability.md b/docs/platform-portability.md index 9f1d033..a8b4c8c 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -27,7 +27,7 @@ or replaced before claiming portable exports. | Area | Current dependency | Runtime impact | Portability status / future action | |---|---|---|---| -| SYS4INI per-game startup profile | `Sys4AssetCatalog` parses and retains the bounded ordered startup trailer; `Sys4LogicalCanvas` applies `SCREENX`/`SCREENY` with AGE's independent `640x480` fallbacks. Native SYS4INI also carries text, ADV input/skip, save ABI/path, audio, legacy renderer, and Windows registration settings | Godot now uses the selected canvas for content scaling, backbuffers/compositor bounds, primary surfaces, layout/input fallbacks, and the matching default windowed client. The port still does not source `CancelMesSkipOnClick`, `CoexistMesSkip`, cursor/redraw policy, or wheel action ids from the trailer | Add later boot parameters that vary only the physical window while Godot scales/letterboxes the unchanged canvas. Apply further semantic keys explicitly, translate save roots through the host, and classify DirectDraw/fullscreen-bit and registration/key settings as native compatibility metadata. Canonical inventory and consumers: `sys4-format-notes.md` and `engine-re.md` | +| SYS4INI per-game startup profile | `Sys4AssetCatalog` parses and retains the bounded ordered startup trailer; `Sys4LogicalCanvas` applies `SCREENX`/`SCREENY` with AGE's independent `640x480` fallbacks. Native SYS4INI also carries text, ADV input/skip, save ABI/path, audio, legacy renderer, and Windows registration settings | Godot uses the selected canvas for content scaling, backbuffers/compositor bounds, primary surfaces, layout/input fallbacks, and the default windowed client. `--window-width`/`--window-height` vary only the physical client while preserving the canvas and letterbox policy. The port still does not source `CancelMesSkipOnClick`, `CoexistMesSkip`, cursor/redraw policy, or wheel action ids from the trailer | Validate the existing presentation policy on Linux/macOS window managers. Apply further semantic keys explicitly, translate save roots through the host, and classify DirectDraw/fullscreen-bit and registration/key settings as native compatibility metadata. Canonical inventory and consumers: `sys4-format-notes.md` and `engine-re.md` | | Retained graphics presentation | Backend-neutral `GfxState`; accepted default Godot `Sprite2D` GPU stage plus the retained software pixel oracle, using runtime `ImageTexture`, canvas transforms/materials, and no native graphics API | GPU backend caches static/color-key variants, updates dynamic surfaces, handles retained range transitions, and falls back whole-frame for the legacy host screen-transition path | Godot owns D3D/Vulkan/Metal/OpenGL selection; validate shader/blend/filter behavior per target rather than adding a platform renderer | | AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED) | `FfmpegMovieDecoder` is the sole factory over the project-owned `native/age_movie_ffmpeg` ABI | Windows-x64 passes the complete 213-payload installed video/audio corpus gate plus audible LOGO/OP/CHAPTER playback | Add target-specific native builds and export packaging | | Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory; video-only streams use monotonic pacing while audio-bearing streams use the Godot output clock | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state; managed code is no longer Windows-annotated, while only the win-x64 native bundle exists today | Add Linux/macOS native builds and smoke gates | diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 1cfe50b..1870855 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -686,9 +686,10 @@ specific data for diagnostics; semantic settings are applied through explicit cr legacy renderer/registration switches remain classified rather than blindly emulated. The first bounded application slice is complete: `SCREENX`/`SCREENY` select the validated logical canvas, all presentation allocations and primary bounds consume it, and it is the default windowed size. Other settings remain on -their existing paths while gameplay is the priority. A second slice will add -independent physical `--window-width`/`--window-height` overrides without changing VM coordinates or -surface geometry. The executable task plan and gates live in `phase-a-slice-plan.md`. This experiment was +their existing paths while gameplay is the priority. The second slice is also complete: +independent physical `--window-width`/`--window-height` overrides change only the windowed client while +the logical canvas, VM coordinates, and surface geometry remain fixed. The executable task plan and gates +live in `phase-a-slice-plan.md`. This experiment was diagnostic only; no Kamidori support or `0x1be` semantics were implemented. ### Other engine versions (SYS3 / SYS5) — one app, not many diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 1d4652d..d7dc619 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -318,6 +318,11 @@ use the last `input-wait` event in `timeline.jsonl` as the authoritative manual - `--scene ` — override the default `SYSTEM4` root with a direct diagnostic scene, e.g. `--scene SC0240` (executes 29 nested subroutines). - `--selftest` — headless; runs a **synthesized** scene through the thread/suspend/`CallDeferred` plumbing and asserts it matches a live headless run (full handling; no vm0/frozen golden). Exits. +- `--window-width ` / `--window-height ` — override only the physical windowed client + dimensions; each omitted dimension defaults independently to the SYS4INI logical canvas. Godot keeps + that logical canvas and aspect-preserving letterboxing, so these options do not change VM coordinates, + AGE surfaces, layout, or backbuffer allocation. Values must be integers in `1..16384`; duplicate + options use the last value. - `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue. - `--boot` — direct-scene diagnostic only: with `--scene `, run the old `INITCONFIG/INIT2/INIT` state prefix before that isolated scene. The normal SYSTEM4-rooted launch neither diff --git a/engine/Age.Engine.Tests/Age.Engine.Tests.csproj b/engine/Age.Engine.Tests/Age.Engine.Tests.csproj index 533b180..c761e82 100644 --- a/engine/Age.Engine.Tests/Age.Engine.Tests.csproj +++ b/engine/Age.Engine.Tests/Age.Engine.Tests.csproj @@ -33,6 +33,7 @@ + diff --git a/engine/Age.Engine.Tests/WindowLaunchOptionsTests.cs b/engine/Age.Engine.Tests/WindowLaunchOptionsTests.cs new file mode 100644 index 0000000..ac6d8f2 --- /dev/null +++ b/engine/Age.Engine.Tests/WindowLaunchOptionsTests.cs @@ -0,0 +1,64 @@ +using Age.Engine.Sys4; + +public class WindowLaunchOptionsTests +{ + private static readonly Sys4LogicalCanvas LogicalCanvas = new(1024, 576); + + [Fact] + public void DefaultsPhysicalWindowToLogicalCanvas() + { + var options = WindowLaunchOptions.Resolve([], LogicalCanvas); + + Assert.Equal(new WindowLaunchOptions(1024, 576, false, false), options); + Assert.False(options.IsOverridden); + } + + [Fact] + public void WidthAndHeightOverrideOnlyPhysicalDimensions() + { + var options = WindowLaunchOptions.Resolve( + ["--scene", "SC0000", "--window-width", "1600", "--window-height", "900"], + LogicalCanvas); + + Assert.Equal(new WindowLaunchOptions(1600, 900, true, true), options); + Assert.Equal(new Sys4LogicalCanvas(1024, 576), LogicalCanvas); + } + + [Theory] + [InlineData("--window-width", "1280", 1280, 576, true, false)] + [InlineData("--window-height", "720", 1024, 720, false, true)] + public void PartialOverrideKeepsOtherLogicalDefault( + string argument, string value, int width, int height, + bool widthOverridden, bool heightOverridden) + { + var options = WindowLaunchOptions.Resolve([argument, value], LogicalCanvas); + + Assert.Equal(new WindowLaunchOptions( + width, height, widthOverridden, heightOverridden), options); + } + + [Fact] + public void DuplicateDimensionUsesLastValue() + { + var options = WindowLaunchOptions.Resolve( + ["--window-width", "1200", "--window-width", "1400"], LogicalCanvas); + + Assert.Equal(1400, options.Width); + Assert.Equal(576, options.Height); + } + + [Theory] + [InlineData("--window-width")] + [InlineData("--window-width", "0")] + [InlineData("--window-height", "-1")] + [InlineData("--window-width", "not-a-number")] + [InlineData("--window-height", "16385")] + [InlineData("--window-width", "--window-height", "720")] + public void InvalidOrMissingDimensionIsRejected(params string[] arguments) + { + var error = Assert.Throws( + () => WindowLaunchOptions.Resolve(arguments, LogicalCanvas)); + + Assert.Contains(arguments[0], error.Message); + } +} diff --git a/godot/Main.cs b/godot/Main.cs index 6ee20a0..6cf5b57 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -22,6 +22,7 @@ public partial class Main : Godot.Control private const int NativeBoldGlyphSpacing = 1; private int _screenWidth = Sys4LogicalCanvas.DefaultWidth; private int _screenHeight = Sys4LogicalCanvas.DefaultHeight; + private WindowLaunchOptions _windowOptions; private TextureRect _screenView = null!; // shows the composited screen backbuffer private Image _screen = null!; // SYS4INI-sized immediate-mode canvas private ImageTexture _screenTex = null!; @@ -108,10 +109,21 @@ public partial class Main : Godot.Control public override void _Ready() { + var userArgs = OS.GetCmdlineUserArgs(); // Resolve the selected game's logical canvas before any presentation allocation. The same catalog // instance is reused for scripts and assets later in startup. var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini); var logicalCanvas = catalog.LogicalCanvas; + try + { + _windowOptions = WindowLaunchOptions.Resolve(userArgs, logicalCanvas); + } + catch (System.ArgumentException error) + { + GD.PushError($"[startup] {error.Message}"); + GetTree().Quit(2); + return; + } _screenWidth = logicalCanvas.Width; _screenHeight = logicalCanvas.Height; _screenPixels = new byte[logicalCanvas.RgbaByteCount]; @@ -120,9 +132,11 @@ public partial class Main : Godot.Control rootWindow.ContentScaleAspect = Window.ContentScaleAspectEnum.Keep; rootWindow.ContentScaleSize = new Vector2I(_screenWidth, _screenHeight); if (rootWindow.Mode == Window.ModeEnum.Windowed) - rootWindow.Size = new Vector2I(_screenWidth, _screenHeight); + rootWindow.Size = new Vector2I(_windowOptions.Width, _windowOptions.Height); GD.Print($"[profile] SYS4INI logical canvas={_screenWidth}x{_screenHeight} " + - $"window={rootWindow.Size.X}x{rootWindow.Size.Y}"); + $"requested window={_windowOptions.Width}x{_windowOptions.Height} " + + $"source={(_windowOptions.IsOverridden ? "boot-arguments" : "logical-canvas")} " + + $"actual={rootWindow.Size.X}x{rootWindow.Size.Y} mode={rootWindow.Mode}"); // One logical canvas that draw-texture blits into, shown behind the dialogue. _screen = Image.CreateEmpty(_screenWidth, _screenHeight, false, Image.Format.Rgba8); @@ -205,7 +219,6 @@ public partial class Main : Godot.Control } _audioOutputLatencySeconds = AudioServer.GetOutputLatency(); - var userArgs = OS.GetCmdlineUserArgs(); _selftest = System.Array.IndexOf(userArgs, "--selftest") >= 0; bool boot = System.Array.IndexOf(userArgs, "--boot") >= 0; // diagnostic prefix for direct-scene runs bool nativeDebugMenu = System.Array.IndexOf(userArgs, "--native-debug-menu") >= 0; @@ -2158,7 +2171,10 @@ public partial class Main : Godot.Control && _screen.GetHeight() == _screenHeight && _screenPixels.Length == checked(_screenWidth * _screenHeight * 4) && rootWindow.ContentScaleSize - == new Vector2I(_screenWidth, _screenHeight); + == new Vector2I(_screenWidth, _screenHeight) + && _windowOptions == WindowLaunchOptions.Resolve( + OS.GetCmdlineUserArgs(), + new Sys4LogicalCanvas(_screenWidth, _screenHeight)); textEffectSmoke.QueueFree(); ok &= launcherOk && sleepMinimumOk && inputTranslationOk && cp932WavMetadataOk && bgmReplacementCancelsFade && textEffectModesOk && fontCalibrationOk @@ -2167,13 +2183,15 @@ public partial class Main : Godot.Control $"debug launcher catalog/UI smoke ({debugEntries.Count} packed scripts); " + $"sleep-min=1ms; native-key-translation=ok; cp932-wav-info=ok; " + $"bgm-fade-replacement=ok; text-effect-modes=ok; font-calibration=ok; " + - $"logical-canvas={_screenWidth}x{_screenHeight}"); + $"logical-canvas={_screenWidth}x{_screenHeight}; " + + $"window-request={_windowOptions.Width}x{_windowOptions.Height}"); else GD.Print($"SELFTEST FAIL: threaded={actual.Count} vs headless={expected.Count}; " + $"debug-launcher={launcherOk}; sleep-min={sleepMinimumOk}; " + $"native-key-translation={inputTranslationOk}; cp932-wav-info={cp932WavMetadataOk}; " + $"bgm-fade-replacement={bgmReplacementCancelsFade}; " + $"text-effect-modes={textEffectModesOk}; font-calibration={fontCalibrationOk}; " + - $"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight})"); + $"logical-canvas={logicalCanvasOk}({_screenWidth}x{_screenHeight}); " + + $"window-request={_windowOptions.Width}x{_windowOptions.Height}"); GetTree().Quit(ok ? 0 : 1); } diff --git a/godot/WindowLaunchOptions.cs b/godot/WindowLaunchOptions.cs new file mode 100644 index 0000000..aef4bb5 --- /dev/null +++ b/godot/WindowLaunchOptions.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Age.Engine.Sys4; + +/// +/// Presentation-only window dimensions resolved from Godot user arguments. These never redefine the +/// SYS4 logical canvas, VM coordinates, or AGE surface dimensions. +/// +public readonly record struct WindowLaunchOptions( + int Width, int Height, bool WidthOverridden, bool HeightOverridden) +{ + public const int MaximumDimension = Sys4LogicalCanvas.MaximumDimension; + public bool IsOverridden => WidthOverridden || HeightOverridden; + + public static WindowLaunchOptions Resolve( + IReadOnlyList arguments, Sys4LogicalCanvas logicalCanvas) + { + ArgumentNullException.ThrowIfNull(arguments); + int width = logicalCanvas.Width; + int height = logicalCanvas.Height; + bool widthOverridden = false; + bool heightOverridden = false; + + for (int index = 0; index < arguments.Count; index++) + { + string argument = arguments[index]; + if (argument is not ("--window-width" or "--window-height")) continue; + if (index + 1 >= arguments.Count) + throw new ArgumentException($"{argument} requires a pixel value"); + + string raw = arguments[++index]; + if (!int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) + || value <= 0 || value > MaximumDimension) + throw new ArgumentException( + $"{argument} must be an integer from 1 through {MaximumDimension}; got '{raw}'"); + + if (argument == "--window-width") + { + width = value; + widthOverridden = true; + } + else + { + height = value; + heightOverridden = true; + } + } + + return new WindowLaunchOptions(width, height, widthOverridden, heightOverridden); + } +}