diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..690d24b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,90 @@ +name: Bug report +description: Something in Theseus or UIX Desktop doesn't work the way it should. +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for reporting. One bug per issue, please. Theseus never ships Xbox system files or game files, so please don't attach any. + - type: dropdown + id: os + attributes: + label: Where are you running it? + options: + - macOS + - Windows + - Linux + - Steam Deck + - Xbox (real hardware) + - Xbox (xemu) + validations: + required: true + - type: input + id: os-version + attributes: + label: OS version and GPU + description: For example "macOS 15.4, M2" or "Fedora 41, RX 6700 XT". The GPU matters most for display problems. + - type: input + id: version + attributes: + label: Theseus version + description: Help > About UIX Desktop shows it. The download's file name carries it too. + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Crash + - Games grid and launching + - Media (library, Plex, Jellyfin, playback) + - Music, sound or visualizer + - Xbox Cloud Gaming and remote play + - Skins, XAP scenes or XIPs + - Display or performance + - Controls + - Xbox dashboard + - Other + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened + description: What you saw. Screenshots help for anything visual. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What you expected + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Open the Games grid + 2. Pick a Steam game + 3. ... + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs and crash reports + description: | + Drag `theseus.log` here from your user data folder. If it crashed, add the newest file from the `crashes` folder inside it, and `theseus.prev.log` if you've started Theseus again since. + + - macOS: `~/Library/Application Support/Theseus` + - Linux and Steam Deck: `$XDG_DATA_HOME/theseus`, or `~/.local/share/theseus` when that isn't set + - Windows: `%APPDATA%\Theseus` (started from a terminal, the log goes to that terminal instead) + + Please don't attach or paste `Configs/desktop.ini`. It holds your Plex, Jellyfin and Xbox sign in tokens. + - type: textarea + id: extra + attributes: + label: Anything else + description: Which download you used (app, AppImage, zip, built from source), controllers, skins, or anything that seems related. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..bcb4012 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Help and questions + url: https://discord.gg/qfVHTYD4xX + about: Setup questions and general chat go to the TeamUIX Discord. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..e12a83a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,52 @@ +name: Feature request +description: An idea for something Theseus doesn't do yet. +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + Thanks for the idea. Anything new has to fit the dashboard's look and render through the 3D scene, so describe the problem first and the solution second. + - type: dropdown + id: build + attributes: + label: Which build is it for? + options: + - UIX Desktop (macOS, Windows, Linux) + - Xbox dashboard + - Both + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Games grid and launching + - Media (library, Plex, Jellyfin, playback) + - Music, sound or visualizer + - Xbox Cloud Gaming and remote play + - Skins, XAP scenes or XIPs + - Controls + - Documentation + - Other + validations: + required: true + - type: textarea + id: problem + attributes: + label: What are you trying to do? + description: The problem or the missing piece, in your own words. + validations: + required: true + - type: textarea + id: idea + attributes: + label: What would you like to see? + description: How it would work from where you sit. Mockups and screenshots welcome. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Anything you've tried instead + description: Workarounds, other apps that do it well, or the original dashboard's behavior if it's relevant. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..011bbef --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,19 @@ +## What this changes + + + +## How you tested it + + + +- [ ] macOS +- [ ] Linux or Steam Deck +- [ ] Windows +- [ ] Xbox (real hardware or xemu) + +## Checklist + +- [ ] `make lint-platform` is clean +- [ ] No scriptable node, FND/PRD table, node name or material name was renamed or removed (scenes and skins rely on them; add alongside instead) +- [ ] Screenshots attached for anything that changes how it looks +- [ ] I'm fine with this being released under GPL-3.0-or-later diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3884f5..6399c69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,13 @@ concurrency: # when authoring new shaders (offline by the contributor). jobs: + lint: + name: Lint / platform flags + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: make -C build lint-platform + desktop-macos: name: Desktop / macOS runs-on: macos-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2936a7..ffbad30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -244,6 +244,22 @@ jobs: tar czf "${DROP}.tar.gz" "${DROP}" echo "ARCHIVE=${DROP}.tar.gz" >> "$GITHUB_ENV" + - name: Package AppImage + # The bare binary needs the runner's exact FFmpeg sonames, which + # newer distros like Debian trixie don't have. The AppImage carries them. + run: | + TAG="${{ github.event.inputs.tag || github.ref_name }}" + sudo apt-get install -y -qq patchelf wget file + chmod +x build/mk-appimage.sh + ./build/mk-appimage.sh ~/builds/theseus/desktop "UIX-Desktop-${TAG}-aarch64.AppImage" + echo "APPIMAGE=UIX-Desktop-${TAG}-aarch64.AppImage" >> "$GITHUB_ENV" + + - name: Upload AppImage to release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + files: ${{ env.APPIMAGE }} + - name: Upload to release uses: softprops/action-gh-release@v2 with: diff --git a/.gitignore b/.gitignore index a97dcc4..9d678c9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ *.wrn *.ilk *.idb +# -MMD writes these beside the compiler's cwd when a build is driven by hand. +*.d Debug/ Release/ Profile/ @@ -68,3 +70,6 @@ theseus/third-party/bgfx/.build/ # xiptool writes .xip next to the source folder on create. The packed # artifact belongs in Data/Xips/, not here. xips-source/*.xip + +# Machine-local build settings (paths to retail XBEs) +build/local.mk diff --git a/Configs/version b/Configs/version index 33196e8..085201d 100644 --- a/Configs/version +++ b/Configs/version @@ -1,2 +1,2 @@ [default] -version=0.5.250605 +version=0.4.0 diff --git a/Data/Skins/Stock/Stock.xbx b/Data/Skins/Stock/Stock.xbx index 7217c30..c49a154 100755 --- a/Data/Skins/Stock/Stock.xbx +++ b/Data/Skins/Stock/Stock.xbx @@ -1,346 +1,20 @@ # -# -# -# # Xbox Dash Skin Material Data # # Skin Name - MS Dash - Green # -# +# Stock is the retail 5960 dashboard. Its colours live in the source +# (theseus/render/materials_retail.inl), so this file lists nothing. +# +# Other skins only need the materials they change. Anything a skin +# leaves out keeps the retail colour. +# # example... # [Some Material Name] <-- Material Name -# ColorA=red,green,blue,alpha <-- Values for material color 1 - usually side faces color -# ColorB=red,green,blue,alpha <-- Values for material color 1 - usually front faces color -# - -[InnerWall_01] -ColorA=243, 255, 107, 255 -ColorB=40, 212, 20, 20 - -[InnerWall_02] -ColorA=243, 255, 107, 255 -ColorB=20, 192, 0, 20 - -[Wireframe] -ColorA=125, 198, 34, 100 -ColorB=125, 198, 34, 0 - -[CellEgg/Parts] -ColorA=243, 255, 107, 178 -ColorB=30, 255, 0, 0 - -[GameEgg] -ColorA=243, 255, 107, 178 -ColorB=30, 255, 0, 0 - -[FlatSurfaces2sided3] -ColorA=253, 30, 0, 255 -ColorB=242, 28, 0, 0 - -[console_hilite] -ColorA=255, 173, 107, 255 -ColorB=246, 255, 0, 0 - -[Metal_Chrome] -ColorA=229, 229, 229, 0 -ColorB=229, 229, 229, 255 - -[Tvbox] -ColorA=229, 229, 229, 0 -ColorB=229, 229, 229, 255 - -[AudioCD] -ColorA=255, 255, 255, 40 -ColorB=255, 255, 255, 180 - -[EggGlowPulse] -ColorA=252, 255, 0, 0 -ColorB=254, 255, 188, 228 - -[EggGlow] -ColorA=252, 255, 0, 0 -ColorB=254, 255, 188, 228 - -[ButtonGlow] -ColorA=252, 255, 0, 0 - -ColorB=254, 255, 188, 228 -[gradient] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[CellWallStructure] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[FlatSrfc/PodParts] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[Cell_Light] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[Cell_Light/LegSkin] -ColorA=191, 255, 107, 51 -ColorB=0, 255, 18, 0 - -[Tubes] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[JewelSurface02/PodMesh] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[TubesFade] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[TubesQ] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[EmptyMU] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[Tube] -ColorA=242, 250, 153, 215 -ColorB=7, 104, 0, 37 - -[MemoryHeader] -ColorA=60, 198, 67, 122 -ColorB=7, 104, 0, 37 - -[FlatSurfaces] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[FlatSurfacesSelected] -ColorA=243, 255, 107, 192 -ColorB=30, 255, 0, 128 - -[FlatSurfacesMemory] -ColorA=243, 255, 107, 192 -ColorB=30, 160, 0, 128 - -[DarkSurfaces] -ColorA=203, 205, 85, 90 -ColorB=20, 192, 0, 0 - -[DarkSurfaces2] -ColorA=203, 205, 85, 90 -ColorB=10, 100, 0, 0 - -[FlatSurfaces2sided] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[DetailLegSkin_Inner] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Screen] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Spout] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[NavType34] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MetaFlatSurfaces] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[HK_SavedGame_Row01] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[SC_SavedGame_Row01] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[SavedEgg_Selected] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[FlatUnselected] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Mem_InnerWall_Outer] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[SavedGameEgg] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[GameMenuEgg] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Shell] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Material #133sdsfdsf] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[IconParts] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[IconParts1] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MU1Pod_HL] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MU1Pod] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[GamePod] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[GamePodb] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[JwlSrfc01/InfoPnls] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[MenuCell] -ColorA=243, 255, 107, 192 -ColorB=20, 192, 0, 0 - -[Material #1334] -Color=11, 32, 0, 192 - -[XBOXgreendark] -Color=6, 33, 0, 255 - -[XBOXgreen] -Color=140, 201, 25, 255 - -[XBoxGreen2] -Color=139, 200, 24, 255 - -[GameHilite33] -Color=221, 208, 120, 178 - -[Nothing] -Color=128, 128, 128, 255 - -[NavType] -Color=190, 250, 94, 178 - -[RedType] -Color=200, 30, 30, 255 - -[XBoxGreen] -Color=139, 200, 24, 255 - -[Type] -Color=100, 200, 25, 255 - -[Typesdsafsda] -Color=255, 255, 255, 179 - -[Material #133] -Color=0, 0, 0, 192 - -[Material #1335] -Color=76, 162, 0, 200 - -[Material #133511] -Color=41, 87, 0, 255 - -[Material #1336] -Color=0, 0, 0, 128 - -[HilightedType] -Color=3, 44, 0, 255 - -[XBoxGreenq] -Color=139, 200, 24, 255 - -[Black80] -Color=0, 0, 0, 204 - -[CellEgg/Partsw] -Color=77, 224, 57, 166 - -[CellEgg/Partsz] -Color=170, 170, 170, 255 - -[Material #108] -Color=160, 252, 0, 255 - -[ItemsType] -Color=182, 245, 96, 255 - -[GameHiliteMemory] -Color=178, 208, 0, 255 - -[red] -Color=255, 0, 0, 255 - -[PanelBacking_01] -Color=4, 20, 0, 255 - -[PanelBacking_02] -Color=0, 0, 0, 255 - -[PanelBacking_03] -Color=4, 20, 0, 240 - -[PanelBacking_04] -Color=14, 46, 7, 255 - -[NameBacking] -Color=1, 8, 0, 255 - -[ModeBacking] -Color=1, 8, 0, 255 - -[SavedGameBacking] -Color=11, 18, 10, 255 - -[MemManMetaBacking] -Color=4, 25, 0, 255 - -[DarkenBacking] -Color=4, 50, 0, 255 - -[TextBacking] -Color=0, 0, 0, 255 - -[MetaBacking] -Color=4, 50, 0, 255 - -[DarkenBackingDark] -Color=1, 8, 0, 255 - -[GameHilite] -Color=255, 255, 255, 255 - -[PanelBacking] -Color=4, 20, 0, 255 - -[Keyboard] -ColorA=178, 208, 0, 255 -ColorB=243, 255, 107, 255 -ColorC=25, 225, 0, 34 -ColorD=190, 250, 94, 0 +# Color=red,green,blue,alpha <-- single colour materials +# ColorA=red,green,blue,alpha <-- falloff side colour +# ColorB=red,green,blue,alpha <-- falloff front colour +# +# [Keyboard] <-- the keyboard keys share one section +# ColorA=Live side, ColorB=side, ColorC=bright front, ColorD=text +# diff --git a/Data/Skins/White/shell.xbx b/Data/Skins/White/shell.xbx new file mode 100644 index 0000000..072108e Binary files /dev/null and b/Data/Skins/White/shell.xbx differ diff --git a/Data/Xips/default-xbox.xip b/Data/Xips/default-xbox.xip index 9478969..254a91c 100644 Binary files a/Data/Xips/default-xbox.xip and b/Data/Xips/default-xbox.xip differ diff --git a/Data/Xips/default.xip b/Data/Xips/default.xip index 27689c3..ec5fc98 100644 Binary files a/Data/Xips/default.xip and b/Data/Xips/default.xip differ diff --git a/LICENSE-THIRD-PARTY.md b/LICENSE-THIRD-PARTY.md index 2a686b1..425dcd1 100644 --- a/LICENSE-THIRD-PARTY.md +++ b/LICENSE-THIRD-PARTY.md @@ -28,23 +28,31 @@ Used by the Xbox build: |---|---|---| | [minimp3](https://github.com/lieff/minimp3) | CC0 (Public Domain) | Single-header MP3 decoder for streaming music playback | -Used by the desktop port: +Used by the desktop build: | Library | License | Purpose | |---|---|---| | [SDL2](https://www.libsdl.org/) | zlib | Window, input, audio platform layer | | [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer) | zlib | Sound playback | -| [libmpv](https://mpv.io/) | LGPL-2.1+ | Video playback for the DVD player node | -| [Dear ImGui](https://github.com/ocornut/imgui) | MIT | Developer tool UI | +| [libmpv](https://mpv.io/) | LGPL-2.1+ | Video playback for the media library and the DVD player node, audio CD playback | +| [libcurl](https://curl.se/libcurl/) | curl (MIT style) | HTTPS for TMDB, Plex, Jellyfin and Xbox sign in | +| [FFmpeg](https://ffmpeg.org/) (libavcodec, libavutil, libswscale) | LGPL-2.1+ | xCloud video decode | +| [Opus](https://opus-codec.org/) | BSD-3-Clause | xCloud audio decode | +| [OpenSSL](https://www.openssl.org/) | Apache-2.0 | TLS and DTLS for the xCloud transport | +| [libdatachannel](https://github.com/paullouisageneau/libdatachannel) (submodule) | MPL-2.0 | WebRTC transport for xCloud and remote play | +| [bgfx](https://github.com/bkaradzic/bgfx), [bx](https://github.com/bkaradzic/bx), [bimg](https://github.com/bkaradzic/bimg) (submodules) | BSD-2-Clause | Rendering on Metal, Direct3D 11 and Vulkan | +| [projectM](https://github.com/projectM-visualizer/projectm) (submodule) | LGPL-2.1+ | MilkDrop music visualizer, linked as a shared library | +| [Dear ImGui](https://github.com/ocornut/imgui) | MIT | Tool window UI | | [ImGuiColorTextEdit](https://github.com/BalazsJako/ImGuiColorTextEdit) | MIT | XAP script editor with syntax highlighting | -| [stb_image](https://github.com/nothings/stb) | Public Domain (or MIT, dual-licensed) | Image loading on the desktop side | -| [GLEW](https://glew.sourceforge.net/) | Modified BSD / MIT | OpenGL extension loader (Windows desktop build only) | +| [imgui-filebrowser](https://github.com/AirGuanZ/imgui-filebrowser) | MIT | File picker in the tool windows | +| [stb_image, stb_image_write](https://github.com/nothings/stb) | Public Domain (or MIT, dual licensed) | Image loading, re-encoding icons and box art | +| [GLEW](https://glew.sourceforge.net/) | Modified BSD / MIT | OpenGL extension loader, linked only by a native MSYS2 Windows build. The release builds don't use it | All of the above are GPL-3.0-compatible, either by being permissive (MIT, BSD, zlib, public domain) or by being LGPL (linkable from GPL). -## Xbox Dashboard Assets (`xboxfs/`) +## Xbox Dashboard Assets (`Data/`) -The XIPs and skin assets in `xboxfs/` are authored by [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite) and contain TeamUIX modifications of dashboard scripts present on every retail Xbox console. The `uixdata` directory layout is a convention established by JbOnE's earlier source-level modification, *User.Interface.X* (UIX); Theseus follows that convention so the same content runs on both projects. UIX Lite's modifications are licensed under GPL-3.0-or-later. The underlying script formats and any unmodified original components are referenced for compatibility and preservation; Microsoft retains its rights to its original work. Every Xbox owner already possesses the equivalent original files on their console. +The XIPs and skin assets in `Data/` are authored by [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite) and contain TeamUIX modifications of dashboard scripts present on every retail Xbox console. The `uixdata` directory layout is a convention established by JbOnE's earlier source-level modification, *User.Interface.X* (UIX); Theseus follows that convention so the same content runs on both projects. UIX Lite's modifications are licensed under GPL-3.0-or-later. The underlying script formats and any unmodified original components are referenced for compatibility and preservation; Microsoft retains its rights to its original work. Every Xbox owner already possesses the equivalent original files on their console. ## Microsoft XDK Headers (`theseus/xdk/`) diff --git a/Library/Applications/README.md b/Library/Applications/README.md index b577f90..3103e0f 100644 --- a/Library/Applications/README.md +++ b/Library/Applications/README.md @@ -2,4 +2,6 @@ On Xbox hardware, this is where homebrew applications live (file managers, tools, utilities), each in its own subfolder containing a `default.xbe`. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate at boot. Drop application folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Applications\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Applications\` is what gets read. diff --git a/Library/Dashboards/README.md b/Library/Dashboards/README.md index 969e1e2..b7c56e5 100644 --- a/Library/Dashboards/README.md +++ b/Library/Dashboards/README.md @@ -1,5 +1,7 @@ # Dashboards -On Xbox hardware, this is where alternate dashboards (UnleashX, Evolution X, XBMC4Xbox, etc.) live, each in its own subfolder containing a `default.xbe`. Theseus itself can also live under here. +On Xbox hardware, this is where alternate dashboards (UnleashX, Evolution X, XBMC4Xbox and friends) live, each in its own subfolder containing a `default.xbe`. Theseus itself can live here too. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate at boot. Drop dashboard folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Dashboards\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Dashboards\` is what gets read. diff --git a/Library/Emulators/README.md b/Library/Emulators/README.md index 02b66f5..d0f15a6 100644 --- a/Library/Emulators/README.md +++ b/Library/Emulators/README.md @@ -1,5 +1,7 @@ # Emulators -On Xbox hardware, this is where emulator apps (Surreal64, FCE Ultra, ScummVM-X, etc.) live, each in its own subfolder containing a `default.xbe` plus its ROM library. +On Xbox hardware, this is where emulator apps (Surreal64, FCE Ultra, ScummVM and friends) live, each in its own subfolder containing a `default.xbe` plus its ROM library. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate at boot. Drop emulator folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Emulators\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Emulators\` is what gets read. diff --git a/Library/Games/README.md b/Library/Games/README.md index c2209f7..8f58dd2 100644 --- a/Library/Games/README.md +++ b/Library/Games/README.md @@ -1,5 +1,7 @@ # Games -On Xbox hardware, this is where game rips and homebrew games live, each in its own subfolder containing a `default.xbe`. The launcher's title scanner walks this tree at boot, parses each XBE's certificate for its title ID, and populates the games panel. +On Xbox hardware, this is where game rips and homebrew games live, each in its own subfolder containing a `default.xbe`. The title scanner walks this tree at boot, reads each XBE's certificate for its title ID and name, and fills the games panel. -This folder is intentionally empty in the public repo. It exists so the desktop build's `E:\` filesystem virtualization has the standard Xbox layout to enumerate. Drop game folders here on desktop to see them in the launcher. On Xbox hardware your real `E:\Games\` is what gets read. +The folder is empty in the repo. It exists so the desktop build's `E:` drive has the standard Xbox layout to list at boot. A folder here on desktop can carry a `default.uixshortcut` file with a `Launch=` line, which the launcher uses when the folder has no `games.ini` entry. + +On desktop, the copy of this folder that counts is the one in your user data folder (`Library/` inside `~/Library/Application Support/Theseus/` on macOS, `~/.local/share/theseus/` on Linux, `%APPDATA%\Theseus\` on Windows), which is seeded from this one on first run. Most entries on desktop come from Title Maker and live in `Configs/games.ini`, so this folder can stay empty. On Xbox hardware your real `E:\Games\` is what gets read. diff --git a/Library/TDATA/README.md b/Library/TDATA/README.md index 39913fa..96a0267 100644 --- a/Library/TDATA/README.md +++ b/Library/TDATA/README.md @@ -1,5 +1,5 @@ # TDATA -Title data. Each Xbox title gets a folder here named by its 8-character title ID, holding save games, per-title configuration, and any persistent state the title writes through `T:\`. On Xbox hardware the kernel maps `T:\` to `E:\TDATA\\` automatically when a title is launched. +Title data. Each Xbox title gets a folder here named by its eight character title ID, holding the persistent state it writes through `T:\` (settings, caches and the like). On Xbox hardware the kernel maps `T:\` to `E:\TDATA\\` when a title launches. Saved games live next door in `UDATA`. -This folder is mostly empty in the public repo. The dashboard's own state lives under `fffe0000` (the dashboard's title ID); see that subfolder's README. On Xbox hardware your real `E:\TDATA\` is what gets read. +The folder is mostly empty in the repo. The dashboard's own state lives under `fffe0000`, the dashboard's title ID; see that subfolder's README. On desktop the live copy is in your user data folder's `Library/TDATA/`. On Xbox hardware your real `E:\TDATA\` is what gets read. diff --git a/README.md b/README.md index 1d2d6db..7df248c 100644 --- a/README.md +++ b/README.md @@ -2,191 +2,153 @@ [![build](https://github.com/MrMilenko/Theseus/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/MrMilenko/Theseus/actions/workflows/build.yml) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE) -[![Platform](https://img.shields.io/badge/platform-Xbox%20%7C%20macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)](#) +[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows%20%7C%20Xbox-lightgrey.svg)](#get-it) [![Discord](https://img.shields.io/discord/1391105896179957830)](https://discord.gg/qfVHTYD4xX) -

- Vulkan -      - Metal -

- -Six years of reverse engineering the original Xbox dashboard. This repo is the result. +The original Xbox shipped with a dashboard nobody has ever quite gotten over: the green glow, the orbs, the hum. Theseus is that dashboard rebuilt plank by plank from years of reverse engineering the retail binaries, and it runs on your computer. -Theseus boots on modded Xbox hardware as a drop in replacement for the stock dashboard. The same engine compiles natively on macOS, Linux, and Windows, where it doubles as **UIX Desktop**: a 3D launcher and media center. The desktop build renders through [bgfx](https://github.com/bkaradzic/bgfx): Metal on macOS, Vulkan on Linux and Windows. +On macOS, Linux and Windows it goes by **UIX Desktop**: a launcher and media center that looks and feels like the 2002 dashboard. Your Steam library, emulators, xemu, Xbox Cloud Gaming, movies, TV and music all live in the games grid and the orbs, driven by the same scene scripts the Xbox ran. -The split is intentional. The Xbox build stays faithful to what you'd expect from the Xbox dashboard (or UIX Lite, if you've used a custom dashboard before). Everything that doesn't belong on an Xbox (Steam libraries, modern video playback, emulator-hosted ISOs, playlists, skin authoring tools) lives on the desktop side instead. Two projects, one engine. - -

- Xbox dashboard - UIX Desktop launcher -

+ UIX Desktop games grid Media library - Custom skin

- Steam tab in Title Maker - RetroArch tab in Title Maker + A custom skin + Steam import in Title Maker

---- - -**Running it:** [Quick Start](#quick-start) · [Features](#features) · [Adding games](#adding-games) · [Where your stuff lives](#where-your-stuff-lives) · [Customization](#customization) · [Controls](#controls-desktop) - -**Working on it:** [Dependencies](#dependencies) · [Building](#building) · [How it works](#how-it-works) · [Heritage](#heritage) · [Credits](#credits) · [Third-party](#third-party-libraries) · [License](#license) - ---- - -## Quick Start - -### Xbox - -1. Grab the latest Xbox release (`default.xbe` + `uixdata/`) -2. Drop the XBE somewhere on the Xbox HDD, e.g. `E:\Dashboards\Theseus\default.xbe` -3. Copy `uixdata/` next to it -4. Copy `Configs/` to `C:\UIX Configs\` -5. Boot it. If something's missing, the panic screen tells you what. +**Jump to:** [Get it](#get-it) · [Set it up](#set-it-up) · [Controls](#controls) · [Getting help](#getting-help) · [Build from source](#build-from-source) · [How it works](#how-it-works) · [The Xbox build](#the-xbox-build) · [Heritage](#heritage) · [Credits](#credits) · [License](#license) -[Download for Xbox ->](https://github.com/MrMilenko/Theseus/releases) +## What's in it -### Desktop +- **Games grid** for anything you can launch: native PC games, scripts, Steam, RetroArch, [ES-DE](https://es-de.org/) collections, Xbox ISOs through [xemu](https://xemu.app), and your Xbox Cloud Gaming library +- **Title Maker** (F3) for adding and editing entries by hand, with import tabs for Steam, RetroArch and ES-DE +- **Media library** for your Movies and TV folders, with posters from [TMDB](https://www.themoviedb.org/), plus [Plex](https://www.plex.tv/) and [Jellyfin](https://jellyfin.org/) servers. Playback runs through libmpv +- **Music** from your own folders, with a [projectM](https://github.com/projectM-visualizer/projectm) (MilkDrop) visualizer +- **Xbox Cloud Gaming and remote play**: sign in with your Xbox account and your cloud games and consoles show up in the grid +- **Skins**: UIX Lite community skins drop in as is, and the Skin Editor helps you make your own +- **CRT effect** for that living room TV look: scanlines, curvature, phosphor mask, bloom, all tunable +- **Controllers**: Xbox, PlayStation and other SDL2 gamepads, plus full keyboard control +- **Xbox HDD browser** (F5) for xemu qcow2 images and their FATX partitions -1. Grab the release for your OS -2. Install the runtime libraries for your platform (see [Dependencies](#dependencies)) -3. Run it +## Get it -Windows is the exception: that release ships with the DLLs it needs, so step 2 is already done for you. macOS and Linux link against system libraries. +Grab the build for your OS from the [Releases page](https://github.com/MrMilenko/Theseus/releases). -[Download for desktop ->](https://github.com/MrMilenko/Theseus/releases) +| OS | Download | Notes | +|---|---|---| +| macOS (Apple Silicon) | `Theseus--macos-app.zip` | Self contained `Theseus.app`. It's ad hoc signed, not notarized, so the first launch needs right click > Open, or **System Settings > Privacy & Security > Open Anyway** on newer macOS. | +| macOS (Apple Silicon) | `Theseus--macos.zip` | Bare binary for people who already live in Homebrew. Needs `brew install sdl2 sdl2_mixer mpv curl ffmpeg opus`. | +| Linux x64 | `UIX-Desktop--x86_64.AppImage` | One file with its libraries bundled. `chmod +x` it and run. This is the one for a Steam Deck. | +| Linux x64 / ARM64 | `Theseus--linux-x64.tar.gz`, `Theseus--linux-arm64.tar.gz` | Binary plus data. Needs the runtime packages listed in [docs/desktop/dependencies.md](docs/desktop/dependencies.md). | +| Windows x64 | `Theseus--windows-x64.zip` | Unzip anywhere and run `theseus.exe`. Every DLL it needs is in the zip. The exe isn't code signed, so SmartScreen may ask first (More info > Run anyway). | -Building from source instead? Jump to [Building](#building). +Rendering goes through [bgfx](https://github.com/bkaradzic/bgfx): Metal on macOS, Direct3D 11 on Windows, Vulkan on Linux. If the default misbehaves on your machine, pick another backend under **Settings > Display > Renderer** and restart. -## Features +## Set it up -### Xbox +### First run -A drop-in replacement for the stock Xbox dashboard on modded consoles. Same look and behavior, because that's what it is. Rebuilt plank by plank and still going. +Launch it and you land on the dashboard. On first run UIX Desktop creates your **user data folder** and copies the starter Configs, Data and Library into it. Everything you change lives there, not next to the app, so updating means replacing the app and nothing else. -- Every original scene, animation, and skin slot -- UIX Lite skins drop in unchanged. Skin authors don't have to do anything -- Hot swap skins from settings, no reboot -- ISO / CCI launching from the harddrive menu, plus the original XBE flow -- Hundreds of titles scan in milliseconds -- Title icons auto populate from each game's XBE certificate -- Quick overlay (LT + B) for ISO loader, file manager, FTP / drive widgets -- FTP server, recovery / panic screen, MP3 soundtrack playback +| OS | User data folder | +|---|---| +| macOS | `~/Library/Application Support/Theseus/` | +| Linux | `$XDG_DATA_HOME/theseus/`, which is usually `~/.local/share/theseus/` | +| Windows | `%APPDATA%\Theseus\` | -### UIX Desktop +**Settings (F4) > General > Open User Directory** takes you straight there. Inside: -The Theseus engine compiled for your computer, with the modern features bolted on. macOS, Linux, Windows, Steam Deck friendly. +| Path | What's there | +|---|---| +| `Configs/games.ini` | Every launcher entry you add: name, title ID, launch command, category | +| `Configs/Icons.ini` | Which icon each entry uses | +| `Configs/icons/` | The icon images, named by title ID (`.jpg` or `.png`) | +| `Configs/desktop.ini` | Desktop settings: display, audio, CRT, library folders, sign ins | +| `Configs/config.ini` | Dashboard settings shared with the Xbox build (skin, clock, language) | +| `Data/` | Skins, orbs, music, fonts, screenshots and the dashboard's scene archives | +| `Library/` | The virtual Xbox `E:` drive: `Games`, `Applications`, `Emulators`, plus title and save data in `TDATA` and `UDATA` | +| `theseus.log`, `theseus.prev.log` | This run's log and the one before it | -- **3D launcher** for native PC games, Steam libraries, RetroArch ROMs, and Xbox ISOs via [xemu](https://xemu.app) -- **xCloud / Game Pass streaming** signs in with your Xbox account and drops your whole cloud library into the games grid, box art and all. Remote play for your own console shows up under "Your Xbox" -- **Media library** that scans your Movies and TV folders, pulls posters from [TMDB](https://www.themoviedb.org/), and plays back through libmpv. [Plex](https://www.plex.tv/) and [Jellyfin](https://jellyfin.org/) servers browse and stream in the same UI (Jellyfin signs in with Quick Connect) -- **Music visualizer** with in-scene [projectM](https://github.com/projectM-visualizer/projectm) (MilkDrop) presets reacting to whatever's playing -- **Skin editor** with live XAP scripting and a scene inspector. Change a skin, see it instantly -- **Title Maker** for adding games and apps, with per launcher import flows for Steam and RetroArch -- **Xbox HDD browser** that opens qcow2 and FATX images -- **CRT post process** for the old TV look. Scanlines, curvature, phosphor, bloom, all tunable -- **Graphics knobs** in Settings > Display: vsync mode, FPS cap, MSAA, hardware video decode -- **Controllers**: Xbox and PlayStation pads via SDL2 +When an update ships new Data (scene archives, fonts, skins), it reaches your copy on the next launch, and anything you've edited is left alone. Only the compiled shaders stay with the app itself. Settings files are written atomically, with the previous version kept beside them as `.old`, so a crash mid save can't eat your games list. -## Adding games +### Adding games and apps -Title Maker (F3 from the dashboard) is where you connect games to dashboard tiles. Three tabs: +Open **Title Maker** with F3 (or Tools > Title Maker). -**Main** is the catch all. Every title you've added shows up here, regardless of which tab created it. This is also where you add the weird stuff that doesn't belong to a launcher: a Windows .exe, a .bat script, a macOS .command file, a shell one liner, anything that takes a path or command. Edit names, swap icons, tweak the launch line. Most of your time managing the library happens here. +- **Main** lists every entry, whatever added it. Create one by hand with a name and a launch line: a path to an executable, a script, a `steam://` link, or any shell command. Quote paths with spaces. **Test Launch** tries it without leaving the dashboard. **Add ISO** turns an Xbox ISO into an xemu entry once the xemu path is set (Find checks the usual install spots). +- **Steam** finds your Steam install, and **Import Steam Library** brings in every installed game with its icon. Add by App ID covers betas and demos. +- **RetroArch** finds your install, and **Import Recent Titles** pulls in what you've played lately with the core and box art already sorted. Or pick a ROM and a core by hand. +- **ES-DE** points at your ES-DE data folder (the one holding `gamelists/`). Your systems and games show up under an ES-DE category, read live on every launch. -**Steam** auto detects your Steam install (Find button), or you point at it once. Hit Import Steam Library and your installed games come in with icons fetched from Valve's CDN. There's also a manual "Add by App ID" form for launching betas, demos, or games not in your normal library scan. +Entries from Main, Steam and RetroArch are stored in **`Configs/games.ini`** inside your user data folder, with their icons in `Configs/icons/` and the icon mapping in `Configs/Icons.ini`. Back those up and you've backed up your launcher. ES-DE and Xbox Cloud Gaming entries are never written there; they're rebuilt each launch from their source. -**RetroArch** detects your RetroArch install the same way. Import Recent Titles pulls in everything you've recently played in RetroArch, with the right core auto resolved and boxart copied from RetroArch's thumbnail packs. You can also add manually: pick a ROM, pick a core from the dropdown, done. +The Steam, RetroArch and ES-DE tabs can be hidden under Optional Tabs at the top of Main. Hiding a tab keeps its entries. When you launch something, the dashboard fades to black and minimizes so the game gets the screen; switch back to it when you're done. Cloud and remote play streams run inside the dashboard window instead. -If you don't use Steam or RetroArch, you can turn either tab off under Optional Tabs (top of Main). Anything you've already added stays in Main either way. +### Xbox Cloud Gaming -## Where your stuff lives +**Settings > Media > Xbox / xCloud > Sign in to Xbox**, then enter the code it shows on any device. From then on your cloud games and your consoles (for remote play) appear in the games grid with their box art. -Everything you can customize sits in a per OS user directory, not next to the -binary. It's created and seeded on first run, and an existing side by side -install is migrated across once. +### Movies, TV and music -| | | -|---|---| -| macOS | `~/Library/Application Support/Theseus/` | -| Linux | `$XDG_DATA_HOME/theseus`, else `~/.local/share/theseus` | -| Windows | `%APPDATA%\Theseus\` | +**Settings > Media > Local Library** takes three folders: -Configs, Library and most of Data live there: skins, orbs, music, screenshots, -fonts, saves and settings. Only the compiled shaders stay with the binary, so a -signed .app, a .deb or a flatpak can ship a read only payload without breaking -anything you own. +- **Movies**: one folder per movie, or video files straight in the folder +- **TV Shows**: `Show Name/Season 1/episodes` (or `Specials`), or episodes straight in the show folder. `S01E02` style names set the order +- **Music**: each subfolder becomes a soundtrack, `.m3u` playlists in the root become soundtracks too, and loose files land in one called Music -**Settings > General > Open User Directory** takes you straight there, which is -the quickest way to check what the app is actually reading. +A free [TMDB API key](https://www.themoviedb.org/settings/api) is optional; with one, posters and synopses fill in. Hit **Refresh Media Library** after changing folders. -On Xbox nothing changed: `C:\UIX Configs\`, `uixdata\` and the rest are where -they always were. +Plex signs in with a PIN code. Jellyfin takes your server URL and signs in with Quick Connect. Both live under **Settings > Media**. -## Customization +### Skins -**Skins.** Drop them into `Data/Skins/` inside the user directory above (Xbox: `uixdata\Skins\`) and pick from settings. UIX Lite community skins work as is, no conversion needed. Hot swap, no reboot. +Skins live in `Data/Skins//` in your user data folder. Pick one from the dashboard's own settings and it swaps live, no restart. UIX Lite community skins work without conversion. A skin's `.xbx` only lists the materials it changes; anything it leaves out keeps the retail 5960 color. **Tools > Skin Editor** can start a new skin from Stock and tweak colors as you watch. -**Scene authoring** (for the people building dashboards from scratch). Scenes are XAP scripts packed into `.xip` archives. The desktop build has a live XAP editor (F2), scene inspector (F1), and asset reload so you can tweak and see results immediately. The XAP node interface is the contract; the C++ behind it can change but the node API is treated as sacred. Full reference in [`docs/xap-contract.md`](docs/xap-contract.md). +### Handy settings -## Controls (desktop) +- **Display**: fullscreen (F11), borderless (F12), resolution, vsync, FPS cap, MSAA, renderer, hardware video decode, and Emulate Xbox Resolution, which frames scenes in the original 720x480 design space +- **CRT Effect**: Settings > Display, or View > CRT Effect. Four presets or eight sliders +- **Audio**: master volume, mute (Ctrl+M), and the projectM visualizer. With it on, X + Y (keys or pad) opens and closes the visualizer window -### Controller +Command line flags: `--fullscreen`, `--4k` (fullscreen plus 2x UI scale), `--scale N`, `--no-toolbar`, `--muted`, `--minimized`, `--dashboard`, `--development`. -Xbox and PlayStation pads work via SDL2 GameController. The dashboard itself is -pad driven as you'd expect; the tool windows have their own mode. +## Controls -| Input | Action | -|---|---| -| **LT + B** | Toggle pad mode. Same chord as the Xbox overlay | -| Stick / D-pad | Move | -| A | Select | -| B | Back out one layer, then close pad mode | -| Y | Open a text field, which brings up the on screen keyboard | -| Hold X | Move and resize windows, for when one outgrows the screen | - -In pad mode the menu bar steps aside for a controller menu, the pad drives the -tools instead of the dashboard, and a prompt bar along the bottom says what the -buttons currently do. On screen keyboard has letters, numbers and a symbol page -for paths. Turn the whole thing off with `PadMode=0` in `Configs/desktop.ini`. - -Skin Editor stays mouse and keyboard only: colour pickers and direct -manipulation don't translate to a stick. Keyboard equivalents: +The dashboard drives like the Xbox did: D-pad or stick to move, A to select, B to back out.
-Dashboard navigation +Keyboard -| Key | Xbox button | Action | -|---|---|---| -| Arrow keys | D-pad | Navigate | -| Enter / Space | A | Select | -| Backspace | B | Back | -| X / Y | X / Y | Context actions | -| Tab | White | Play / Pause | -| ` (backtick) | Black | Stop | -| WASD | Left stick | Analog navigation | +| Key | Xbox button | +|---|---| +| Arrow keys | D-pad | +| WASD | Left stick | +| Enter / Space | A | +| Backspace / Esc / B | B | +| X / Y | X / Y | +| Tab | White | +| ` (backtick) | Black | +| Q / E | Left / right trigger |
-Media playback +Video playback | Key | Action | |---|---| -| Esc / Q | Stop, return to dashboard | -| Space | Pause / Resume | -| Left / Right | Seek 5s | -| `[` / `]` | Previous / Next in playlist | -| T | Track picker (audio + subtitles) | +| Esc / Q / Backspace | Stop and return to the dashboard | +| Space | Pause / resume | +| Left / Right | Seek 5 seconds | +| `[` / `]` | Previous / next in playlist | +| T | Audio and subtitle tracks |
-Tools (desktop only) +Tools and windows | Key | Action | |---|---| @@ -196,252 +158,119 @@ manipulation don't translate to a stick. Keyboard equivalents: | F4 | Settings | | F5 | Xbox HDD browser | | F6 | Playlist Maker | -| F10 | Toggle menu bar | -| F11 | Toggle fullscreen | +| F10 | Show or hide the menu bar | +| F11 | Fullscreen | +| F12 | Borderless window | | Ctrl+M | Mute | -| Ctrl+R | Restart dashboard | - -
- ---- - -# For developers - -The rest is build instructions, architecture notes, and the lineage. Skip if you just want to run it. - -## Dependencies - -One list, used by both the [Quick Start](#quick-start) and [Building](#building). "Run" is what a downloaded release needs; "Build" is that plus the toolchain and headers. - -**macOS (Homebrew):** +| Ctrl+R | Restart the dashboard | -``` -# Run a release -brew install sdl2 sdl2_mixer mpv curl ffmpeg opus - -# Build from source (adds the toolchain + libdatachannel's deps) -brew install sdl2 sdl2_mixer mpv curl ffmpeg opus pkg-config cmake openssl@3 -``` - -**Linux (Debian / Ubuntu):** - -``` -# Run a release -sudo apt install libsdl2-2.0-0 libsdl2-mixer-2.0-0 libmpv2 libcurl4 libopus0 - -# Build from source -sudo apt install build-essential pkg-config cmake \ - libsdl2-dev libsdl2-mixer-dev \ - libvulkan-dev libx11-dev libgl-dev \ - libmpv-dev libcurl4-openssl-dev libssl-dev \ - libavcodec-dev libavutil-dev libswscale-dev libopus-dev -``` - -**Windows (MSYS2 / MinGW64):** - -``` -pacman -S make pkg-config mingw-w64-x86_64-gcc \ - mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_mixer \ - mingw-w64-x86_64-mpv mingw-w64-x86_64-curl \ - mingw-w64-x86_64-vulkan-headers mingw-w64-x86_64-vulkan-loader -``` +On a Mac, Cmd works in place of Ctrl. -Notes: - -- Some distros ship `libmpv1` instead of `libmpv2`. Either works. -- The FFmpeg runtime libs come in with libmpv. `ffmpeg` / `libopus0` cover the xCloud video and audio decoders. -- OpenSSL is a runtime dependency of xCloud. On macOS it arrives with brew's curl / mpv / ffmpeg, and on Debian `libssl3` is already in the base install, so neither needs it listed for the run case. Building libdatachannel does need the headers, which is why `openssl@3` and `libssl-dev` show up above. - -## Building - -Both builds live in this repo and use the same Makefile. - -### Xbox - -Cross-compiles from macOS or Linux using clang + lld-link + cxbe. Requires: -- clang and lld-link (`brew install llvm` on macOS, `apt install clang lld` on Linux) -- [OXDK](https://github.com/MrMilenko/OXDK) cloned and built -- An Xbox SDK source tree (path passed as `XDK_BASE`) - -``` -git clone https://github.com/MrMilenko/OXDK ~/OXDK -cd ~/OXDK/tools/cxbe && make -cd /path/to/Theseus/build -make CONFIG=retail XDK_BASE=/path/to/xbox -``` - -Output lands at `~/builds/theseus/xbox-retail/default.xbe`. - -The Xbox target is not built in CI, because the SDK tree it needs isn't something this repo can ship. Verify it locally before you ship anything. - -### Desktop - -Same source tree, different Makefile target. Needs C++17 and the packages in [Dependencies](#dependencies). Rendering goes through bgfx: Metal on macOS, Vulkan on Linux and Windows. The compiled shader binaries live in-tree under `Data/shaders/`, so a normal build never touches `shaderc`. - -**1. Init the submodules** (once): - -``` -git submodule update --init --recursive -``` + -**2. Build the bgfx runtime libraries** (once per platform): +
+Controller in the tool windows -Only three libs are needed. bgfx's own top level targets also build `shaderc`, -`geometryc` and a debug config you'll never load, which takes far longer and -drags in tools this build doesn't use. +**LT + B** (the same chord as the Xbox overlay) hands the pad to the tool windows. A prompt bar along the bottom shows what each button does right now. -``` -cd theseus/third-party/bgfx +| Input | Action | +|---|---| +| Stick / D-pad | Move | +| A | Select | +| B | Back out one layer, then leave pad mode | +| Y | Open a text field with the on screen keyboard | +| Hold X | Move and resize windows | -# macOS (Apple Silicon) -../bx/tools/bin/darwin/genie --gcc=osx-arm64 gmake -make -C .build/projects/gmake-osx-arm64 -j bx bimg bgfx config=release +Turn it off with `PadMode=0` in `Configs/desktop.ini`. The Skin Editor stays mouse and keyboard only. -# Linux -../bx/tools/bin/linux/genie --gcc=linux-gcc gmake -make -C .build/projects/gmake-linux-gcc -j bx bimg bgfx config=release64 +
-# Windows (MSYS2 mingw shell, or cross-compiled from Linux) -../bx/tools/bin/windows/genie.exe --gcc=mingw-gcc gmake -make -C .build/projects/gmake-mingw-gcc -j bx bimg bgfx config=release64 -``` +## Getting help -bx ships `genie` prebuilt, and on an older distro it fails with -`GLIBC_2.38 not found`. Build it from source and carry on: +Found a bug? Open an [issue](https://github.com/MrMilenko/Theseus/issues) or find us on the TeamUIX [Discord](https://discord.gg/qfVHTYD4xX). Please include: -``` -git clone --depth 1 https://github.com/bkaradzic/GENie.git /tmp/genie -make -C /tmp/genie -cp /tmp/genie/bin/linux/genie theseus/third-party/bx/tools/bin/linux/genie -``` +1. **Your OS and version**, and your GPU if it's a display problem +2. **Which download you're running**: the file name carries the version and package. Help > About UIX Desktop shows it too +3. **What you did, what you expected, and what happened instead** +4. **`theseus.log`** from your user data folder (see [First run](#first-run)). If it crashed on the previous launch, attach `theseus.prev.log` as well. A crash also leaves a report in the `crashes` folder there; attach the newest one -**3. Build libdatachannel** (once per platform, macOS and Linux). +On Windows, starting `theseus.exe` from a terminal sends the log to that terminal instead of the file. -This one is easy to skip and the failure is quiet: the Makefile only defines `THESEUS_HAVE_WEBRTC` when it finds the static lib, so without this step everything compiles and links fine and xCloud is simply gone from the build. The directory name matters, `build-mac` on macOS and `build-linux` on Linux, because that's where the Makefile looks. +Please don't post `Configs/desktop.ini` as is: it holds your Plex, Jellyfin and Xbox sign in tokens. -``` -cd theseus/third-party/libdatachannel - -# macOS -cmake -S . -B build-mac -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ - -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ - -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" -cmake --build build-mac -j - -# Linux -cmake -S . -B build-linux -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ - -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 -cmake --build build-linux -j -``` +## Build from source -Windows cross builds stub xCloud out, so this step doesn't apply there. +The full walkthrough is in [docs/desktop/building.md](docs/desktop/building.md), and the libraries per OS are in [docs/desktop/dependencies.md](docs/desktop/dependencies.md). The shape of it on macOS or Linux: -**4. Build the dashboard:** +1. `git clone --recursive https://github.com/MrMilenko/Theseus` +2. Install the build packages for your OS. Everything is found through `pkg-config`, so any package manager works +3. Build the bgfx runtime libraries and libdatachannel once. Skip libdatachannel and xCloud quietly drops out of the build +4. `cd build && make desktop`, then run `~/builds/theseus/desktop/theseus` -``` -# macOS / Linux -cd build && make desktop -~/builds/theseus/desktop/theseus +Windows builds cross compile with mingw (`make desktop-win64`), and there's a Linux ARM64 cross target (`make desktop-linux-arm64`). Scripts for the macOS `.app` and the Linux AppImage live in `build/`. -# Windows (MSYS2 / MinGW64) -cd build && make desktop-win64 -``` +For contributors: only `theseus/desktop/host_*` talks to the operating system. `make lint-platform` enforces that, and `make win64-import-guard` catches stray Win32 calls. CI runs the lint and builds macOS, Linux and Windows on every push. -`make desktop` also builds the vendored projectM (MilkDrop visualizer) into a local prefix on first run. Only re-run the `shaders-bgfx*` Makefile targets if you edit a `.sc` shader source; the checked-in `.bin` files cover every backend. +## How it works -### macOS .app +The dashboard is a scene graph driven by XAP, a small scripting language compiled to bytecode, with scenes, meshes and textures packed into `.xip` archives. Theseus reimplements the engine underneath: the script VM, the VRML flavored scene graph with its FND/PRD reflection tables, asset loading, text, materials, audio and input. The scripts and archives run unmodified, which is why the scene node interface is treated as a contract ([docs/xap-contract.md](docs/xap-contract.md)). -`build/mk-macapp.sh` wraps a desktop build into `Theseus.app`: it copies the -Homebrew dylibs into `Contents/Frameworks`, rewrites their install names to -`@rpath` so the bundle runs on a machine without your brew layout, builds the -icon set, and ad-hoc signs the result. +On desktop, the dashboard's Direct3D 8 calls land on a thin shim that drives bgfx, Xbox drive paths map onto your user data folder, and SDL2 handles windows, input and sound. ``` -cd build && make desktop -./build/mk-macapp.sh +theseus/ + engine/ Pure logic: script compiler, math, locale + shared/ Both builds: script VM, files and titles, settings, keyboard + render/ Both builds: scene graph, materials, shapes, text + desktop/ Desktop only: SDL main loop, bgfx shim, host_* OS layer, tools, media, launchers + xbox/ Xbox only: kernel and hardware glue + toolbox/ Xbox only: FTP and drive tools from the PrometheOS lineage +theseuslib/ C library: XISO and XIP parsers +xips-source/ Editable source for the scene archives Theseus adds or changes ``` -Lands at `~/builds/theseus/Theseus.app`. Ad-hoc signing is enough to run it -locally; handing it to anyone else needs a Developer ID and notarization. - -### Steam Deck and AppImage +Documentation index: [docs/README.md](docs/README.md). What we found in the dashboard binary, subsystem by subsystem: [docs/decomp/](docs/decomp/). -Currently, AppImages are provided for SteamDeck/SteamOS users. This hasn't been fully tested. Please reach out on the TeamUIX [Discord](https://discord.gg/qfVHTYD4xX). +## The Xbox build -Cross-compiling for Windows from macOS / Linux, ARM64 Linux, or any of the more involved setups is in [`docs/desktop/`](docs/desktop/). The CI workflow builds all four desktop targets (macOS, Linux x64, Linux ARM64, Windows) on every push, which is the closest thing to executable docs for the one-time setup. +The same engine still compiles for the original Xbox as a replacement dashboard, built with [OXDK](https://github.com/MrMilenko/OXDK) through `build/Makefile.xbox`. It's a work in progress and isn't part of the desktop releases. If you want a custom dashboard on your console today, use [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite), where TeamUIX's console work currently lives. -## How it works - -The engine is approximately 50 source files reconstructed from the retail and patched XBE's spanning 4920 to 5960, organized the same way the original dashboard was: script VM, scene graph, rendering, asset loading, UI framework, system integration, and launcher. Per-subsystem reverse engineering notes are in [`docs/decomp/`](docs/decomp/). - -The XAP scripting layer is a custom JS-like bytecode VM. The scene graph is VRML97-inspired with runtime reflection via FND/PRD property tables. On the desktop side, D3D8 calls translate through a thin shim into bgfx, which targets Metal on macOS and Vulkan on Linux and Windows. Everything else compiles for both targets from the same shared source. +

+ Theseus on the Xbox +

-If you're poking around the source, the high-level layout: +Building it needs clang and lld-link, OXDK, and an Xbox SDK tree of your own linked at `theseus/xdk` (it isn't in this repo): ``` -theseus/ - engine/ Pure logic (VM, nodes, math) - shared/ Cross-platform with Win32 types (file I/O, audio, settings) - render/ Scene graph, materials, shapes - xbox/ Xbox-only (XTL, modchip, kernel APIs) - desktop/ Desktop-only (SDL, bgfx, ImGui tools) - toolbox/ PrometheOS-derived FTP / drive / network (Xbox-only) -theseuslib/ Shared C library (xiso parser, xip parser) +cd build +make -f Makefile.xbox CONFIG=retail # or CONFIG=debug ``` -Heavier docs index lives at [`docs/README.md`](docs/README.md). +The XBE lands in `~/builds/theseus/xbox-retail/` (or `xbox-debug/`). On the console it expects `uixdata\` beside the XBE, mounted as `Q:`, and its configs in `C:\UIX Configs\`. If something's missing at boot, the panic screen tells you what. ## Heritage -Theseus is part of the TeamUIX lineage. JbOnE created *User.Interface.X* (UIX), a source level modification of the original Xbox Dashboard, which we also poked around in via Ghidra to figure certain things out. Modern TeamUIX continues that tradition with [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite) (a heavily patched retail XBE) and Theseus (this repository). +Theseus is part of the TeamUIX lineage. JbOnE created *User.Interface.X* (UIX), a source level modification of the original Xbox dashboard, and a lot of what we know about how the dashboard behaves came from years of living with it. Modern TeamUIX carries that on with [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite), a heavily patched retail dashboard, and with Theseus. -There's a circularity to it. UIX modified the dashboard at the source level. Theseus reaches the same destination from the other side of the river, rebuilding the codebase from binary analysis and untangling changes made to XIPs over 25 years of community modification. +UIX changed the dashboard from the inside. Theseus reaches the same place from the other side of the river: Ghidra analysis of the retail dashboard binaries from 4920 through 5960, custom Ghidra scripts that pull the dashboard's own FND/PRD descriptor tables out of the binary, disassembly where the decompiler gets it wrong, every name checked against the XAP scripts that call it, and two decades of community changes to the XIPs untangled along the way. The findings are written up per subsystem in [docs/decomp/](docs/decomp/). -For the broader UIX project narrative, see [UIX History](https://github.com/MrMilenko/UIX-History). +For the wider UIX story, see [UIX History](https://github.com/MrMilenko/UIX-History). ## Credits -**Team UIX:** -- **Milenko**: primary RE and development, UIX Desktop port +**TeamUIX** +- **Milenko**: reverse engineering and development, UIX Desktop - **BigJx**: UIX Lite XIPs (XAP scripts, skins, scene assets), testing, bug reports -- **Rocky5**: skin presets and the Colourizer XBE color patcher (technique descends from **ZogoChieftan**'s in-dashboard color patching in BlackStormX, circa 2004) -- **JbOnE**: original UIX, the lineage Theseus continues - -**Upstream code:** -- [Team Resurgent](https://github.com/Team-Resurgent): [PrometheOS](https://github.com/Team-Resurgent/PrometheOS-Firmware) (the toolbox is forked from here via [UIX Lite Toolbox](https://github.com/OfficialTeamUIX/UIX-Lite-Toolbox)) and [Hermes](https://github.com/Team-Resurgent/Hermes) (ISO/CCI mount support) +- **Rocky5**: skin presets and the Colourizer XBE color patcher (the technique descends from **ZogoChieftan**'s in dashboard color patching in BlackStormX, circa 2004) +- **JbOnE**: the original UIX, the lineage Theseus continues -## Third-party libraries +**Upstream code** +- [Team Resurgent](https://github.com/Team-Resurgent): [PrometheOS](https://github.com/Team-Resurgent/PrometheOS-Firmware) (the Xbox toolbox is forked from it via [UIX Lite Toolbox](https://github.com/OfficialTeamUIX/UIX-Lite-Toolbox)) and [Hermes](https://github.com/Team-Resurgent/Hermes) (ISO/CCI mounting on Xbox) -Xbox build: - -| Library | License | Purpose | -|---|---|---| -| [minimp3](https://github.com/lieff/minimp3) | CC0 | MP3 decoder for the music system | - -Desktop build: - -| Library | License | Purpose | -|---|---|---| -| [SDL2](https://www.libsdl.org/) | zlib | Window, input, audio | -| [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer) | zlib | Sound playback | -| [libmpv](https://mpv.io/) | LGPL 2.1+ | Video playback | -| [libcurl](https://curl.se/libcurl/) | curl | HTTPS for TMDB metadata | -| [Dear ImGui](https://github.com/ocornut/imgui) | MIT | Developer tool UI | -| [ImGuiColorTextEdit](https://github.com/BalazsJako/ImGuiColorTextEdit) | MIT | XAP script editor with syntax highlighting | -| [stb_image](https://github.com/nothings/stb) | Public Domain | Image loading | -| [bgfx](https://github.com/bkaradzic/bgfx) | BSD 2-Clause | Cross-platform render abstraction (Metal, Vulkan) | -| [bx](https://github.com/bkaradzic/bx) | BSD 2-Clause | bgfx base library | -| [bimg](https://github.com/bkaradzic/bimg) | BSD 2-Clause | bgfx image utility library | -| [projectM](https://github.com/projectM-visualizer/projectm) | LGPL 2.1+ | MilkDrop music visualizer | -| [FFmpeg](https://ffmpeg.org/) (libavcodec / libswscale / libavutil) | LGPL 2.1+ | xCloud video decode | -| [Opus](https://opus-codec.org/) | BSD 3-Clause | xCloud audio decode | -| [libdatachannel](https://github.com/paullouisageneau/libdatachannel) | MPL 2.0 | WebRTC transport for xCloud / remote play (one-time local build, see [Building](#desktop-1)) | -| [GLEW](https://glew.sourceforge.net/) | Modified BSD / MIT | OpenGL extension loader (projectM / milkdrop visualizer, Windows only) | - -Full catalog with attributions is in [`LICENSE-THIRD-PARTY.md`](LICENSE-THIRD-PARTY.md). +**Libraries**: SDL2, SDL2_mixer, libmpv, libcurl, FFmpeg, Opus, OpenSSL, libdatachannel, bgfx, bx, bimg, projectM, Dear ImGui, ImGuiColorTextEdit, imgui-filebrowser, stb and minimp3. Licenses and links are in [LICENSE-THIRD-PARTY.md](LICENSE-THIRD-PARTY.md). ## License -Theseus is licensed under the **GNU General Public License, version 3 or later** (`GPL-3.0-or-later`). Full license text in [`LICENSE`](LICENSE). +Theseus is licensed under the **GNU General Public License, version 3 or later** (`GPL-3.0-or-later`). Full text in [LICENSE](LICENSE). -Inherited code keeps its origin license intact (`theseus/toolbox/` from PrometheOS via UIX Lite Toolbox is GPL-3.0; Hermes is GPL-3.0). The XIPs and skin assets shipped in `Data/` are TeamUIX's UIX Lite work and ship under GPL-3.0-or-later. +Inherited code keeps its own license (`theseus/toolbox/` from PrometheOS via UIX Lite Toolbox is GPL-3.0; Hermes is GPL-3.0). The XIPs and skin assets shipped in `Data/` are TeamUIX's UIX Lite work and ship under GPL-3.0-or-later. diff --git a/build/Makefile b/build/Makefile index 49656f2..32ce600 100644 --- a/build/Makefile +++ b/build/Makefile @@ -27,25 +27,42 @@ # Requirements (per target) # ------------------------------------------------------------------------ # Xbox (debug/retail): -# - clang + lld-link (`brew install llvm` on macOS, `apt install clang lld` -# on Linux). Apple clang from the Xcode CLT works on macOS too, as long -# as lld is available via Homebrew llvm. +# - clang + lld-link. Debian `apt install clang lld`, Fedora `dnf install +# clang lld`, Arch `pacman -S clang lld`, macOS `brew install llvm` or +# `port install clang-17 lld-17`. Apple clang from the Xcode CLT works +# on macOS too, as long as an lld-link is on PATH. # - OXDK cloned and built: https://github.com/MrMilenko/OXDK # - An Xbox SDK source tree (headers + libs the dashboard ABI needs at # link time). Path passed via XDK_BASE. # # Native desktop: # - C++17 compiler (clang or gcc). -# - SDL2, SDL2_mixer, OpenGL headers. -# macOS: brew install sdl2 sdl2_mixer -# Debian/Ubuntu: sudo apt install build-essential libsdl2-dev \ -# libsdl2-mixer-dev libgl-dev -# Arch: sudo pacman -S sdl2 sdl2_mixer -# MSYS2 (Windows): pacman -S mingw-w64-x86_64-{gcc,SDL2,SDL2_mixer} +# - pkg-config. every library below is located through it, so no specific +# package manager is assumed. Install the deps however you like and, if +# they sit off the default search path, export PKG_CONFIG_PATH. +# - SDL2, SDL2_mixer, libmpv, libcurl, ffmpeg, opus, OpenGL headers. +# Debian/Ubuntu: sudo apt install build-essential pkg-config \ +# libsdl2-dev libsdl2-mixer-dev \ +# libmpv-dev libcurl4-openssl-dev \ +# libavcodec-dev libavutil-dev \ +# libswscale-dev libopus-dev libgl-dev +# Fedora: sudo dnf install gcc-c++ pkgconf-pkg-config \ +# SDL2-devel SDL2_mixer-devel \ +# mpv-libs-devel libcurl-devel \ +# ffmpeg-devel opus-devel +# Arch: sudo pacman -S base-devel pkgconf sdl2 sdl2_mixer \ +# mpv curl ffmpeg opus +# macOS: brew install sdl2 sdl2_mixer mpv curl ffmpeg opus \ +# pkg-config +# (or: sudo port install libsdl2 libsdl2_mixer mpv \ +# curl ffmpeg libopus pkgconfig) +# MSYS2 (Windows): pacman -S pkg-config mingw-w64-x86_64-{gcc,SDL2,SDL2_mixer,mpv,curl} # # Windows cross (mingw-w64): -# - macOS: brew install mingw-w64 -# Linux: sudo apt install mingw-w64 +# - Debian/Ubuntu: sudo apt install mingw-w64 +# Fedora: sudo dnf install mingw64-gcc-c++ +# Arch: sudo pacman -S mingw-w64-gcc +# macOS: brew install mingw-w64 # - Plus the SDL2 + SDL2_mixer mingw devel tarballs and a locally built # mingw GLEW. See the desktop-win64 section header below for the # one-time setup script. @@ -71,6 +88,9 @@ # ------------------------------------------------------------------------ # XDK_BASE=/path/to/xbox MS XDK source tree (Xbox builds) # OXDK_DIR=/path/to/OXDK OXDK toolkit (Xbox builds) +# PKG_CONFIG=pkg-config Override to use a wrapper or cross variant +# PKG_CONFIG_PATH=/path/pkgconfig Where to find .pc files off the default path +# BGFX_BUILD_TAG=osx-arm64 bgfx .build/ subdir, if not the host default # BUILDS_ROOT=/path/to/output Where build dirs live (default: ~/builds/theseus) # SDL2_MINGW_DIR=/path/to/dir SDL2 mingw devel root (Windows cross) # GLEW_MINGW_DIR=/path/to/dir GLEW source root (Windows cross) @@ -97,7 +117,15 @@ NTOS_INC = $(XDK_BASE)/private/ntos/inc NTOS_XAPI = $(XDK_BASE)/private/ntos/xapi/inc SDK_INC = $(XDK_BASE)/public/sdk/inc DDK_INC = $(XDK_BASE)/public/ddk/inc -CXBE = $(OXDK_DIR)/tools/cxbe/cxbe +# OXDK moved its Xbox bits under xbox/. Take the new path, fall back to the old. +# Never leave this empty: the next token in the XBE recipe starts with '-', which +# make would eat as the ignore-errors prefix and report a successful build that +# produced nothing. An unbuilt cxbe has to fail loudly, naming the path it wanted. +CXBE := $(firstword $(wildcard $(OXDK_DIR)/xbox/tools/cxbe/cxbe \ + $(OXDK_DIR)/tools/cxbe/cxbe)) +ifeq ($(strip $(CXBE)),) +CXBE := $(OXDK_DIR)/xbox/tools/cxbe/cxbe +endif # ============================================================ # Build configuration (debug vs retail) @@ -284,6 +312,7 @@ SRCS_SHARED = \ shared/timezone.cpp \ shared/title_collection.cpp \ shared/util.cpp \ + shared/frame_profiler.cpp \ shared/xap_vm.cpp \ shared/xbe.cpp @@ -296,7 +325,6 @@ SRCS_SHARED_XBOX_ONLY = \ SRCS_RENDER = \ render/animation_nodes.cpp \ render/asset_loader.cpp \ - render/autosmooth.cpp \ render/camera.cpp \ render/hud_node.cpp \ render/materials.cpp \ @@ -307,7 +335,8 @@ SRCS_RENDER = \ render/text.cpp \ render/tmap_system.cpp \ render/widget_draw.cpp \ - render/widget_layer.cpp + render/widget_layer.cpp \ + render/xip_archive.cpp SRCS_XBOX = \ xbox/config.cpp \ @@ -329,8 +358,7 @@ SRCS_XBOX = \ xbox/theseus_launcher.cpp \ xbox/title_scanner.cpp \ xbox/widget_config.cpp \ - xbox/xbox_live.cpp \ - xbox/xip_archive.cpp + xbox/xbox_live.cpp SRCS_TOOLBOX = \ toolbox/drive.cpp \ @@ -423,6 +451,50 @@ clean: @# Sweep up the in-tree leftovers from the old layout if present. rm -rf bin bin-retail bin-desktop +# Platform flags live in shared/platform.h. The old desktop spellings stay +# retired, and code the Xbox build compiles never reads _WIN32, which is +# true on the Xbox too. On desktop only the host layer (desktop/host_*) +# talks to the OS by name: is host_win* only, the compiler's OS +# macros are platform.h and host_* only, and THESEUS_HOST_* is too, apart +# from LINT_HOST_DATA. Those files keep per-OS data, not OS calls: default +# install paths for Steam, RetroArch and xemu, and the Linux CD ioctls. +# Vendored headers (stb, minimp3, imfilebrowser, imgui) are left alone. +LINT_DIRS = $(THESEUS_SRC)/desktop $(THESEUS_SRC)/shared $(THESEUS_SRC)/engine \ + $(THESEUS_SRC)/render $(THESEUS_SRC)/toolbox +LINT_VENDOR = --exclude-dir=third-party --exclude-dir=xdk --exclude-dir=imgui \ + --exclude='stb_*.h' --exclude='minimp3.h' --exclude='imfilebrowser.h' +LINT_HOST_DATA = title_maker.cpp retroarch.cpp steam.cpp hdd_browser.cpp cdaudio.cpp +LINT_HOSTTEST = THESEUS_HOST_(WIN|MAC|LINUX) +.PHONY: lint-platform +lint-platform: + @bad=0; \ + if grep -rnwE '_DESKTOP|UIX_DESKTOP|_WINDOWS' $(THESEUS_SRC) \ + --include='*.h' --include='*.cpp' --include='*.c' \ + --exclude-dir=third-party --exclude-dir=xdk --exclude-dir=imgui; then \ + echo "lint-platform: retired flag above. Use _XBOX or THESEUS_DESKTOP."; bad=1; \ + fi; \ + if grep -rnE '^[[:space:]]*#[[:space:]]*include[[:space:]]*<(windows|winsock2|ws2tcpip|shellapi|commdlg|shlobj|dbghelp)[.]h>' \ + $(LINT_DIRS) --include='*.h' --include='*.cpp' --include='*.c' $(LINT_VENDOR) \ + | grep -v '/desktop/host_win[^/]*[.]cpp:'; then \ + echo "lint-platform: Windows header above. Only desktop/host_win*.cpp may include it."; bad=1; \ + fi; \ + if grep -rnwE '_WIN32|__APPLE__|__linux__' $(LINT_DIRS) --include='*.h' --include='*.cpp' --include='*.c' \ + $(LINT_VENDOR) --exclude='platform.h' --exclude='host_*'; then \ + echo "lint-platform: OS macro above. _WIN32 is true on the Xbox too; go through host.h."; bad=1; \ + fi; \ + if grep -rnE '$(LINT_HOSTTEST)' $(LINT_DIRS) \ + --include='*.h' --include='*.cpp' --include='*.c' $(LINT_VENDOR) --exclude='platform.h' \ + --exclude='host_*' $(foreach f,$(LINT_HOST_DATA),--exclude='$(f)'); then \ + echo "lint-platform: host test above. Go through host.h."; bad=1; \ + fi; \ + for f in $$(grep -rlE '$(LINT_HOSTTEST)' $(THESEUS_SRC)/desktop \ + --include='*.h' --include='*.cpp' $(LINT_VENDOR) --exclude='host_*'); do \ + awk '/^[ \t]*#[ \t]*include[ \t]*"(platform|std)[.]h"/ { ok = 1 } \ + /$(LINT_HOSTTEST)/ { if (!ok) { print FILENAME ":" FNR ": host test before platform.h"; exit 1 } exit 0 }' $$f \ + || { echo "lint-platform: include platform.h before testing THESEUS_HOST_*."; bad=1; }; \ + done; \ + [ $$bad -eq 0 ] && echo "lint-platform: clean"; exit $$bad + # ============================================================ # Desktop build (macOS / Linux / Windows) # ============================================================ @@ -443,11 +515,69 @@ DESKTOP_TARGET = $(DESKTOP_DIR)/theseus # Detect platform UNAME_S := $(shell uname -s) UNAME_M := $(shell uname -m) + +# ------------------------------------------------------------------ +# Dependency discovery +# ------------------------------------------------------------------ +# pkg-config is the portable path and is what every platform uses. No +# package manager is required or assumed: Homebrew, MacPorts, apt, pacman, +# nix, MSYS2 and a hand-built prefix all work the same way, because all of +# them install .pc files. If yours are somewhere unusual, point +# PKG_CONFIG_PATH at them and nothing else needs to change. +# +# Override PKG_CONFIG to use a wrapper or a cross-prefixed pkg-config. +PKG_CONFIG ?= pkg-config + +# macOS convenience only: Homebrew keeps some formulae keg-only, so their +# .pc files sit off the default search path. If brew happens to be +# installed, fold those directories in. Skipped entirely when it is not, +# and a PKG_CONFIG_PATH you set yourself always wins. +ifeq ($(UNAME_S),Darwin) + BREW ?= $(shell command -v brew 2>/dev/null) + ifneq ($(BREW),) + BREW_KEG_PC := $(shell sep=""; for f in openssl@3 curl ffmpeg opus; do \ + d=$$($(BREW) --prefix $$f 2>/dev/null)/lib/pkgconfig ; \ + [ -d "$$d" ] || continue ; \ + printf '%s%s' "$$sep" "$$d" ; sep=":" ; \ + done) + endif +endif + +# Every probe below is a parse-time $(shell), and make 3.81 (the Xcode CLT make) +# exports variables to recipe subshells ONLY -- an `export PKG_CONFIG_PATH` here +# would never reach them. So put the path on the command line instead. +ifeq ($(strip $(BREW_KEG_PC)),) + PKGC := $(PKG_CONFIG) +else + # Append to an existing PKG_CONFIG_PATH, but never introduce an empty element. + ifeq ($(strip $(PKG_CONFIG_PATH)),) + PKGC_PATH := $(BREW_KEG_PC) + else + PKGC_PATH := $(PKG_CONFIG_PATH):$(BREW_KEG_PC) + endif + PKGC := PKG_CONFIG_PATH="$(PKGC_PATH)" $(PKG_CONFIG) + export PKG_CONFIG_PATH := $(PKGC_PATH) +endif + +# yes/empty. use with ifeq to gate on a package being present. +pkg_have = $(shell $(PKGC) --exists $(1) 2>/dev/null && echo yes) + +# SDL2. sdl2-config ships with every SDL2 install and is the most reliable +# probe; pkg-config is the fallback for distros that drop the script. +ifneq ($(shell command -v sdl2-config 2>/dev/null),) + SDL2_CFLAGS := $(shell sdl2-config --cflags) + SDL2_LDFLAGS := $(shell sdl2-config --libs) +else ifeq ($(call pkg_have,sdl2),yes) + SDL2_CFLAGS := $(shell $(PKGC) --cflags sdl2) + SDL2_LDFLAGS := $(shell $(PKGC) --libs sdl2) +else + SDL2_CFLAGS := + SDL2_LDFLAGS := -lSDL2 +endif + ifeq ($(UNAME_S),Darwin) DESKTOP_CXX ?= clang++ DESKTOP_CC ?= clang - SDL2_CFLAGS := $(shell sdl2-config --cflags 2>/dev/null || echo -I/opt/homebrew/include/SDL2 -I/usr/local/include/SDL2) - SDL2_LDFLAGS := $(shell sdl2-config --libs 2>/dev/null || echo -L/opt/homebrew/lib -lSDL2) # OpenGL is for projectM (the milkdrop visualizer), not a dashboard # render backend; the dashboard renders through bgfx (Metal here). DESKTOP_PLATFORM_LDFLAGS = -framework OpenGL -lSDL2_mixer @@ -456,8 +586,6 @@ else ifeq ($(UNAME_S),Linux) # clang installed can still override with `make desktop DESKTOP_CXX=clang++`. DESKTOP_CXX ?= g++ DESKTOP_CC ?= gcc - SDL2_CFLAGS := $(shell sdl2-config --cflags) - SDL2_LDFLAGS := $(shell sdl2-config --libs) # OpenGL is for projectM, not a dashboard render backend. DESKTOP_PLATFORM_LDFLAGS = -lGL -lSDL2_mixer else @@ -469,8 +597,6 @@ else # dbghelp: sdl_main.cpp's StackWalk64-based panic crash handler. DESKTOP_CXX ?= g++ DESKTOP_CC ?= gcc - SDL2_CFLAGS := $(shell sdl2-config --cflags) - SDL2_LDFLAGS := $(shell sdl2-config --libs) DESKTOP_PLATFORM_LDFLAGS = -lopengl32 -lglew32 -lSDL2_mixer -lws2_32 -ldbghelp endif @@ -491,21 +617,25 @@ endif # target. (Detected via MAKECMDGOALS.) DESKTOP_NATIVE_GOALS := desktop desktop-clean ifneq ($(filter $(DESKTOP_NATIVE_GOALS),$(or $(MAKECMDGOALS),desktop)),) - ifeq ($(shell pkg-config --exists mpv 2>/dev/null && echo yes),yes) - MPV_CFLAGS := $(shell pkg-config --cflags mpv) - MPV_LDFLAGS := $(shell pkg-config --libs mpv) + ifeq ($(call pkg_have,mpv),yes) + MPV_CFLAGS := $(shell $(PKGC) --cflags mpv) + MPV_LDFLAGS := $(shell $(PKGC) --libs mpv) else - $(error libmpv (mpv.pc) not found via pkg-config. Install: macOS \ -`brew install mpv`; Debian/Ubuntu `sudo apt install libmpv-dev`; \ -MSYS2 `pacman -S mingw-w64-x86_64-mpv`) + $(error libmpv (mpv.pc) not found by $(PKG_CONFIG). Debian/Ubuntu \ +`apt install libmpv-dev`; Fedora `dnf install mpv-libs-devel`; Arch \ +`pacman -S mpv`; MSYS2 `pacman -S mingw-w64-x86_64-mpv`; macOS \ +`brew install mpv` or `port install mpv`. Built it yourself? Add its \ +pkgconfig dir to PKG_CONFIG_PATH) endif - ifeq ($(shell pkg-config --exists libcurl 2>/dev/null && echo yes),yes) - CURL_CFLAGS := $(shell pkg-config --cflags libcurl) - CURL_LDFLAGS := $(shell pkg-config --libs libcurl) + ifeq ($(call pkg_have,libcurl),yes) + CURL_CFLAGS := $(shell $(PKGC) --cflags libcurl) + CURL_LDFLAGS := $(shell $(PKGC) --libs libcurl) else - $(error libcurl (libcurl.pc) not found via pkg-config. Install: macOS \ -`brew install curl`; Debian/Ubuntu `sudo apt install libcurl4-openssl-dev`; \ -MSYS2 `pacman -S mingw-w64-x86_64-curl`) + $(error libcurl (libcurl.pc) not found by $(PKG_CONFIG). Debian/Ubuntu \ +`apt install libcurl4-openssl-dev`; Fedora `dnf install libcurl-devel`; Arch \ +`pacman -S curl`; MSYS2 `pacman -S mingw-w64-x86_64-curl`; macOS \ +`brew install curl` or `port install curl`. Built it yourself? Add its \ +pkgconfig dir to PKG_CONFIG_PATH) endif # libprojectM is vendored as a git submodule under # theseus/third-party/projectm and built into a local prefix by the @@ -536,11 +666,6 @@ DESKTOP_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_DESKTOP \ - -DTHESEUS_DESKTOP \ - -DUIX_DESKTOP=1 \ - -D_WINDOWS=1 \ - -D_THESEUS_STD_H=1 \ -fpermissive \ -fms-extensions \ $(SDL2_CFLAGS) \ @@ -563,7 +688,6 @@ DESKTOP_CFLAGS = \ -Wno-dangling-else \ -Wno-format \ -Wno-microsoft-exception-spec \ - -Wno-macro-redefined \ -Wno-microsoft \ -Wno-null-conversion \ -Wno-c++11-narrowing \ @@ -578,20 +702,28 @@ DESKTOP_LDFLAGS = $(SDL2_LDFLAGS) $(DESKTOP_PLATFORM_LDFLAGS) $(MPV_LDFLAGS) $(C # xCloud video/audio decode uses libavcodec/libswscale + opus directly # (headers already come in via mpv's pkg-config cflags). ffmpeg is a hard # dep of the desktop anyway. -ifeq ($(UNAME_S),Darwin) - DESKTOP_LDFLAGS += -L$(shell brew --prefix ffmpeg)/lib -lavcodec -lavutil -lswscale - DESKTOP_CFLAGS += -I$(shell brew --prefix opus)/include - DESKTOP_LDFLAGS += -L$(shell brew --prefix opus)/lib -lopus -else - DESKTOP_CFLAGS += $(shell pkg-config --cflags opus) - DESKTOP_LDFLAGS += $(shell pkg-config --libs libavcodec libavutil libswscale opus) +# Gated on the native desktop goals: the Xbox and cross targets never consume +# these flags (the cross builds carry their own ffmpeg/opus), and a top-level +# $(error) here would abort them -- and `clean` -- at parse time. +XCLOUD_PKGS := libavcodec libavutil libswscale opus +ifneq ($(filter $(DESKTOP_NATIVE_GOALS),$(or $(MAKECMDGOALS),desktop)),) + ifeq ($(call pkg_have,$(XCLOUD_PKGS)),yes) + DESKTOP_CFLAGS += $(shell $(PKGC) --cflags $(XCLOUD_PKGS)) + DESKTOP_LDFLAGS += $(shell $(PKGC) --libs $(XCLOUD_PKGS)) + else + $(error ffmpeg/opus ($(XCLOUD_PKGS)) not found by $(PKG_CONFIG). \ +Debian/Ubuntu `apt install libavcodec-dev libavutil-dev libswscale-dev libopus-dev`; \ +Fedora `dnf install ffmpeg-devel opus-devel`; Arch `pacman -S ffmpeg opus`; \ +macOS `brew install ffmpeg opus` or `port install ffmpeg opus`) + endif endif # Xbox remote play / xCloud streaming uses libdatachannel for the WebRTC # transport (ICE + DTLS + SCTP datachannels + RTP media). Static libs come # from a one time build under third-party/libdatachannel: # cmake -S . -B build-mac -DBUILD_SHARED_LIBS=OFF -DNO_WEBSOCKET=1 \ -# -DNO_EXAMPLES=1 -DNO_TESTS=1 -DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3) +# -DNO_EXAMPLES=1 -DNO_TESTS=1 +# (add -DOPENSSL_ROOT_DIR=... if cmake cannot find OpenSSL on its own) # cmake --build build-mac -j # The block is skipped (no THESEUS_HAVE_WEBRTC) if the lib is not built yet. LDC_ROOT := $(THESEUS_SRC)/third-party/libdatachannel @@ -608,8 +740,8 @@ ifneq ($(wildcard $(LDC_BUILD)/libdatachannel.a),) $(LDC_BUILD)/deps/libjuice/libjuice.a \ $(LDC_BUILD)/deps/libsrtp/libsrtp2.a \ $(LDC_BUILD)/deps/usrsctp/usrsctplib/libusrsctp.a - ifeq ($(UNAME_S),Darwin) - DESKTOP_LDFLAGS += -L$(shell brew --prefix openssl@3)/lib -lssl -lcrypto + ifeq ($(call pkg_have,libssl libcrypto),yes) + DESKTOP_LDFLAGS += $(shell $(PKGC) --libs libssl libcrypto) else DESKTOP_LDFLAGS += -lssl -lcrypto endif @@ -629,10 +761,28 @@ endif BGFX_ROOT := $(THESEUS_SRC)/third-party/bgfx BX_ROOT := $(THESEUS_SRC)/third-party/bx BIMG_ROOT := $(THESEUS_SRC)/third-party/bimg + +# bgfx names its output dir after the config you built with, so the host +# arch has to pick the same name genie did. Intel Macs land in osx-x64, +# Apple Silicon in osx-arm64. Override BGFX_BUILD_TAG if you built with a +# different genie target. +ifeq ($(UNAME_S),Darwin) + ifeq ($(UNAME_M),arm64) + BGFX_BUILD_TAG ?= osx-arm64 + else + BGFX_BUILD_TAG ?= osx-x64 + endif +else ifeq ($(UNAME_S),Linux) + # bx has no arm64 target dir: genie --gcc=linux-gcc + config=release64 writes + # linux64_gcc on aarch64 as well. See bx/scripts/toolchain.lua. + BGFX_BUILD_TAG ?= linux64_gcc +else + BGFX_BUILD_TAG ?= win64_mingw-gcc +endif DESKTOP_CFLAGS += -DTHESEUS_USE_BGFX=1 \ -I$(BGFX_ROOT)/include -I$(BX_ROOT)/include -I$(BIMG_ROOT)/include ifeq ($(UNAME_S),Darwin) - BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/osx-arm64/bin + BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/$(BGFX_BUILD_TAG)/bin DESKTOP_CFLAGS += -I$(BX_ROOT)/include/compat/osx DESKTOP_LDFLAGS += $(BGFX_BUILD_DIR)/libbgfxRelease.a \ $(BGFX_BUILD_DIR)/libbxRelease.a \ @@ -644,7 +794,7 @@ else ifeq ($(UNAME_S),Linux) # make -C theseus/third-party/bgfx linux-gcc-release64 # bgfx on Linux uses Vulkan by default. Runtime needs vulkan-loader # + X11 system libs; -ldl/-lpthread are for bgfx's RTLD use. - BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/linux64_gcc/bin + BGFX_BUILD_DIR := $(BGFX_ROOT)/.build/$(BGFX_BUILD_TAG)/bin DESKTOP_CFLAGS += -I$(BX_ROOT)/include/compat/linux DESKTOP_LDFLAGS += $(BGFX_BUILD_DIR)/libbgfxRelease.a \ $(BGFX_BUILD_DIR)/libbxRelease.a \ @@ -660,13 +810,26 @@ DESKTOP_SRCS_ENGINE = \ $(THESEUS_SRC)/engine/xap_compile.cpp \ $(THESEUS_SRC)/render/node_system.cpp \ $(THESEUS_SRC)/shared/util.cpp \ + $(THESEUS_SRC)/shared/frame_profiler.cpp \ $(THESEUS_SRC)/engine/array.cpp \ $(THESEUS_SRC)/shared/string_util.cpp \ $(THESEUS_SRC)/engine/math_node.cpp \ $(THESEUS_SRC)/shared/date_node.cpp +# The host layer: one per OS, picked by host. host_win.cpp is the only +# desktop file that sees . +HOST_WIN_SRCS = $(THESEUS_SRC)/desktop/host_win.cpp $(THESEUS_SRC)/desktop/host_win_app.cpp +HOST_POSIX_SRCS = $(THESEUS_SRC)/desktop/host_posix.cpp $(THESEUS_SRC)/desktop/host_posix_app.cpp +ifeq ($(filter Darwin Linux,$(UNAME_S)),) + DESKTOP_SRCS_HOST = $(HOST_WIN_SRCS) +else + DESKTOP_SRCS_HOST = $(HOST_POSIX_SRCS) +endif + DESKTOP_SRCS_FILE = \ - $(THESEUS_SRC)/desktop/xip.cpp \ + $(DESKTOP_SRCS_HOST) \ + $(THESEUS_SRC)/desktop/xbox_file.cpp \ + $(THESEUS_SRC)/render/xip_archive.cpp \ $(THESEUS_SRC)/shared/file_util.cpp \ $(THESEUS_SRC)/desktop/files.cpp \ $(THESEUS_SRC)/shared/activefile.cpp \ @@ -674,13 +837,11 @@ DESKTOP_SRCS_FILE = \ DESKTOP_SRCS_RENDER = \ $(THESEUS_SRC)/shared/screen.cpp \ - $(THESEUS_SRC)/desktop/image.cpp \ - $(THESEUS_SRC)/desktop/texture.cpp \ + $(THESEUS_SRC)/render/asset_loader.cpp \ $(THESEUS_SRC)/render/tmap_system.cpp \ - $(THESEUS_SRC)/desktop/materials_desktop.cpp \ - $(THESEUS_SRC)/desktop/mesh.cpp \ + $(THESEUS_SRC)/render/materials.cpp \ $(THESEUS_SRC)/render/shape_render.cpp \ - $(THESEUS_SRC)/desktop/text.cpp \ + $(THESEUS_SRC)/render/text.cpp \ $(THESEUS_SRC)/render/starfield.cpp \ $(THESEUS_SRC)/engine/dotfield_node.cpp @@ -698,12 +859,11 @@ DESKTOP_SRCS_SYSTEM = \ $(THESEUS_SRC)/desktop/dashinit.cpp \ $(THESEUS_SRC)/desktop/config.cpp \ $(THESEUS_SRC)/desktop/joystick.cpp \ - $(THESEUS_SRC)/desktop/keyboard.cpp \ - $(THESEUS_SRC)/desktop/locale.cpp \ + $(THESEUS_SRC)/shared/keyboard_node.cpp \ + $(THESEUS_SRC)/engine/locale_node.cpp \ $(THESEUS_SRC)/shared/timezone.cpp \ $(THESEUS_SRC)/desktop/debug.cpp \ - $(THESEUS_SRC)/desktop/memutil.cpp \ - $(THESEUS_SRC)/render/autosmooth.cpp + $(THESEUS_SRC)/desktop/memutil.cpp # Desktop-only platform sources (SDL window, ImGui, audio, tools) DESKTOP_SRCS_PLATFORM = \ @@ -717,8 +877,6 @@ DESKTOP_SRCS_PLATFORM = \ $(THESEUS_SRC)/desktop/launchers/retroarch.cpp \ $(THESEUS_SRC)/desktop/title_sanitize.cpp \ $(THESEUS_SRC)/desktop/udata_synth.cpp \ - $(THESEUS_SRC)/xbox/modchip.cpp \ - $(THESEUS_SRC)/xbox/network.cpp \ $(THESEUS_SRC)/desktop/desktop_stubs.cpp \ $(THESEUS_SRC)/desktop/desktop_nodes.cpp \ $(THESEUS_SRC)/desktop/media_db.cpp \ @@ -786,18 +944,7 @@ DESKTOP_DEPS = $(DESKTOP_OBJS:.o=.d) # Run manually with `make -C build shaders-bgfx[-dx11]`; not yet # wired into the desktop rebuild flow since shader edits are # infrequent and require a separate run anyway. -ifeq ($(UNAME_S),Darwin) - ifeq ($(UNAME_M),arm64) - SHADERC_BUILD_DIR := osx-arm64 - else - SHADERC_BUILD_DIR := osx-x64 - endif -else ifeq ($(UNAME_S),Linux) - SHADERC_BUILD_DIR := linux64_gcc -else - SHADERC_BUILD_DIR := win64_mingw-gcc -endif -SHADERC ?= $(THESEUS_SRC)/third-party/bgfx/.build/$(SHADERC_BUILD_DIR)/bin/shadercRelease +SHADERC ?= $(BGFX_ROOT)/.build/$(BGFX_BUILD_TAG)/bin/shadercRelease SHADERS_SRC_DIR := $(THESEUS_SRC)/desktop/shaders SHADERS_OUT_DIR := $(THESEUS_SRC)/../Data/shaders BGFX_SHADER_INC := $(THESEUS_SRC)/third-party/bgfx/src @@ -1077,11 +1224,6 @@ WIN64_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_DESKTOP \ - -DTHESEUS_DESKTOP \ - -DUIX_DESKTOP=1 \ - -D_WINDOWS=1 \ - -D_THESEUS_STD_H=1 \ -DSDL_MAIN_HANDLED \ -DUIX_CDAUDIO=1 \ -fpermissive \ @@ -1170,7 +1312,8 @@ ifneq ($(wildcard $(LDC_WIN64)/libdatachannel.a),) # and the link fails on __imp_ prefixed names against the static archive. WIN64_CFLAGS += -DTHESEUS_HAVE_WEBRTC=1 -DRTC_STATIC \ -I$(LDC_ROOT)/include \ - -I$(MINGW_DEPS)/include + -I$(MINGW_DEPS)/include \ + -I$(MINGW_DEPS)/include/opus # avcodec before swscale before avutil: static archives resolve left to right. WIN64_LDFLAGS += $(LDC_WIN64)/libdatachannel.a \ $(LDC_WIN64)/deps/libjuice/libjuice.a \ @@ -1190,8 +1333,9 @@ endif WIN64_SRCS = $(filter-out \ $(THESEUS_SRC)/desktop/imgui/imgui_impl_opengl3.cpp \ - $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp, $(DESKTOP_ALL_SRCS)) -WIN64_SRCS += $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp + $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp \ + $(DESKTOP_SRCS_HOST), $(DESKTOP_ALL_SRCS)) +WIN64_SRCS += $(THESEUS_SRC)/desktop/imgui_impl_bgfx.cpp $(HOST_WIN_SRCS) # Same source list as desktop. the compiler reads the same files. WIN64_OBJS = $(patsubst $(THESEUS_SRC)/%.cpp,$(WIN64_DIR)/%.o,$(WIN64_SRCS)) @@ -1281,6 +1425,27 @@ $(WIN64_DIR)/%.o: $(THESEUS_SRC)/%.cpp @mkdir -p $(dir $@) $(WIN64_CXX) $(WIN64_CFLAGS) -c -o $@ $< +# Import guard: only the host_win objects may call kernel32's file, clock +# and sleep calls. Anything else that does got a real Win32 call where the +# shim was meant to answer. Compiles the objects, doesn't link, so it runs +# without the mingw libs. +WIN64_NM ?= x86_64-w64-mingw32-nm +WIN64_GUARD_SYMS = CreateFileA CreateFileW DeleteFileA DeleteFileW \ + CreateDirectoryA CreateDirectoryW GetDiskFreeSpaceExA GetDiskFreeSpaceExW \ + FindFirstFileA FindFirstFileW GetTickCount Sleep +WIN64_GUARD_OBJS = $(filter-out $(WIN64_DIR)/desktop/host_win%.o,$(WIN64_OBJS)) + +.PHONY: win64-import-guard +win64-import-guard: $(WIN64_GUARD_OBJS) + @bad=0; \ + for o in $(WIN64_GUARD_OBJS); do \ + hits=$$($(WIN64_NM) -u $$o | awk '{ print $$NF }' | sed 's/^__imp_//' | \ + grep -xF $(foreach s,$(WIN64_GUARD_SYMS),-e $(s)) | sort -u | tr '\n' ' '); \ + if [ -n "$$hits" ]; then echo "$$o: $$hits"; bad=1; fi; \ + done; \ + if [ $$bad -ne 0 ]; then echo "win64-import-guard: Win32 calls above belong in desktop/host_win*.cpp."; exit 1; fi; \ + echo "win64-import-guard: clean ($(words $(WIN64_GUARD_OBJS)) objects)" + desktop-win64-clean: projectm-win64-clean rm -rf $(WIN64_DIR) @@ -1343,11 +1508,6 @@ LINUX_ARM64_CFLAGS = \ -std=c++17 \ $(if $(DEBUG),-g -O0,-O2 -DNDEBUG) \ -MMD -MP \ - -D_DESKTOP \ - -DTHESEUS_DESKTOP \ - -DUIX_DESKTOP=1 \ - -D_WINDOWS=1 \ - -D_THESEUS_STD_H=1 \ -DUIX_CDAUDIO=1 \ -fpermissive \ -I$(LINUX_ARM64_SDL2_INC) \ diff --git a/build/Makefile.xbox b/build/Makefile.xbox new file mode 100644 index 0000000..b217c15 --- /dev/null +++ b/build/Makefile.xbox @@ -0,0 +1,217 @@ +# Theseus, original Xbox build. +# +# The toolchain lives in OXDK. This file says what Theseus is made of and +# where its own quirks are; everything about how to drive clang, lld-link and +# cxbe for an Xbox target comes from $(OXDK_DIR)/oxdk.mk. Fixes to OXDK's +# flags reach Theseus by updating OXDK, not by hand-editing a second copy. +# +# Not invoked directly, normally. `make debug` / `make retail` in build/ +# delegate here. Driving it by hand works too: +# +# make -f Makefile.xbox CONFIG=retail +# +# Requires: clang + lld-link on PATH, OXDK cloned, and an XDK tree at +# $(XDK_DIR) (headers in include/, libs in lib/). cxbe builds itself. + +THESEUS_SRC ?= ../theseus +BUILDS_ROOT ?= $(HOME)/builds/theseus +CONFIG ?= debug + +# ------------------------------------------------------------------ +# OXDK +# ------------------------------------------------------------------ +# Prefer a vendored copy so a fresh clone needs nothing extra, then a +# sibling checkout, then the conventional ~/OXDK. +OXDK_DIR ?= $(firstword $(wildcard $(THESEUS_SRC)/third-party/oxdk/oxdk.mk \ + ../OXDK/oxdk.mk \ + $(HOME)/OXDK/oxdk.mk)) +OXDK_DIR := $(patsubst %/oxdk.mk,%,$(OXDK_DIR)) + +ifeq ($(strip $(OXDK_DIR)),) +$(error OXDK not found. Clone it next to Theseus or into $$HOME: \ +git clone https://github.com/MrMilenko/OXDK ~/OXDK \ +-- or set OXDK_DIR=/path/to/OXDK. cxbe builds itself from there.) +endif + +# OXDK reads headers from $(XDK_DIR)/include and libs from $(XDK_DIR)/lib, +# which is the layout theseus/xdk already has. +XDK_DIR ?= $(THESEUS_SRC)/xdk + +ifeq ($(wildcard $(XDK_DIR)/include),) +$(error No XDK at $(XDK_DIR). theseus/xdk is a gitignored symlink to a local \ +XDK tree with include/ and lib/ -- point it at yours, or pass XDK_DIR=/path) +endif + +# ------------------------------------------------------------------ +# Config +# ------------------------------------------------------------------ +ifeq ($(CONFIG),retail) + OUTPUT_DIR := $(BUILDS_ROOT)/xbox-retail + XBE_MODE := RETAIL + # Release libs. no debug asserts, no XBDM dependency. + XGRAPHICS_LIB := xgraphics.lib + XAPILIB := xapilib.lib + XNET_LIB := xnet.lib + XONLINE_LIB := xonline.lib + CRT_LIB := libcmt.lib + CRTCPP_LIB := libcpmt.lib + XBDM_LIB := + OXDK_OPT := -O0 +else + OUTPUT_DIR := $(BUILDS_ROOT)/xbox-debug + XBE_MODE := DEBUG + # Debug libs, except dsound: debug dsound asserts without a debugger attached. + XGRAPHICS_LIB := xgraphicsd.lib + XAPILIB := xapilibd.lib + XNET_LIB := xnetd.lib + XONLINE_LIB := xonlined.lib + CRT_LIB := libcmtd.lib + CRTCPP_LIB := libcpmtd.lib + XBDM_LIB := xbdm.lib + OXDK_OPT := -O0 -g +endif + +# OXDK defaults to -O2, which is right for a 733MHz console but wrong here: +# clang's optimizer miscompiles this codebase for Xbox and the retail XBE +# crashes on hardware. Both configs stay at -O0 until that is tracked down. +# Set OXDK_OPT on the command line to experiment. + +XBE_TITLE := theseus +XBE_NAME := Theseus + +# ------------------------------------------------------------------ +# Sources +# ------------------------------------------------------------------ +include xbox-sources.mk + +# d3d_compat.cpp lives beside this Makefile, not under theseus/. +SRCS := $(addprefix $(THESEUS_SRC)/,$(ALL_SRCS)) d3d_compat.cpp + +# theseuslib: the private-API helpers and the xiso/xip parsers, built as a +# static lib so outside consumers can link it on its own. +THESEUSLIB_DIR := ../theseuslib +THESEUSLIB_SRCS := theseuslib.c xiso.c xip_archive.c +THESEUSLIB_OBJS := $(addprefix $(OUTPUT_DIR)/theseuslib/,$(THESEUSLIB_SRCS:.c=.obj)) +THESEUSLIB_DEPS := $(THESEUSLIB_OBJS:.obj=.d) +THESEUSLIB_LIB := $(OUTPUT_DIR)/theseuslib.lib + +# ------------------------------------------------------------------ +# Theseus-specific flags, layered on top of OXDK's +# ------------------------------------------------------------------ +# -MMD -MP: OXDK does not track header deps, and we want incremental builds. +# xdk_compat.h: Theseus's own shim. It maps the public XDK headers onto the +# NT types this code expects, which is why no private XDK tree is required. +THESEUS_XBOX_FLAGS = \ + -MMD -MP \ + -D_UNICODE \ + -DUNICODE \ + -D_LIGHTS \ + -D_MAC_CROSS_COMPILE \ + -include xdk_compat.h \ + -I. \ + -iquote $(THESEUS_SRC)/xbox \ + -iquote $(THESEUS_SRC) \ + -iquote $(THESEUS_SRC)/engine \ + -iquote $(THESEUS_SRC)/shared \ + -iquote $(THESEUS_SRC)/render \ + -I$(THESEUS_SRC)/xbox/compat + +# Machine-local settings, never committed. Set DASH_IDENTITY_XBE there to a +# retail dashboard XBE and the built XBE takes its certificate identity (see +# xbe_identity.py). Without it the XBE keeps cxbe's defaults. +-include local.mk + +# Experiment switches from the command line, e.g. EXTRA_DEFINES=-DTHESEUS_PUSHBUFFER_KB=2048 +THESEUS_XBOX_FLAGS += $(EXTRA_DEFINES) + +CFLAGS += $(THESEUS_XBOX_FLAGS) +# OXDK leaves RTTI on because libcmt can support it. Theseus does not use +# dynamic_cast or typeid and the dashboard has no room for the tables. +CXXFLAGS += $(THESEUS_XBOX_FLAGS) -fno-rtti + +# Dashboard-only imports, on top of the kernel set OXDK already declares. +LDFLAGS += \ + /map:$(OUTPUT_DIR)/$(XBE_TITLE).map \ + /alternatename:__imp__KeDelayExecutionThread=__imp__KeDelayExecutionThread@12 \ + /alternatename:_ExReadWriteRefurbInfo=_ExReadWriteRefurbInfo@12 \ + /alternatename:_MU_CreateDeviceObject=_MU_CreateDeviceObject@12 \ + /alternatename:_MU_CloseDeviceObject=_MU_CloseDeviceObject@8 \ + /alternatename:_XcHMAC=_XcHMAC@28 \ + /alternatename:___CxxFrameHandler3=___CxxFrameHandler \ + /alternatename:_XapiDeleteCachePartition=_XapiDeleteCachePartition@4 \ + /alternatename:?vswprintf@@YAHPA_WIPB_WPAD@Z=_vswprintf \ + /alternatename:_mp3dec_init@4=_mp3dec_init \ + /alternatename:_mp3dec_decode_frame@20=_mp3dec_decode_frame \ + /alternatename:_mp3_decode_full@20=_mp3_decode_full + +# Full lib list, not OXDK's default: the config split above picks debug or +# release variants, and xapilib.lib follows $(XAPILIB) deliberately so the +# debug build still resolves the retail-only entry points. +OXDK_LIBS := \ + xboxkrnl.lib \ + $(XAPILIB) \ + xapilib.lib \ + d3d8.lib \ + d3dx8.lib \ + $(XGRAPHICS_LIB) \ + dsound.lib \ + $(XNET_LIB) \ + $(XONLINE_LIB) \ + $(CRT_LIB) \ + $(CRTCPP_LIB) \ + $(XBDM_LIB) + +LIBS += $(THESEUSLIB_LIB) + +# ------------------------------------------------------------------ +# OXDK supplies the rules from here down +# ------------------------------------------------------------------ +include $(OXDK_DIR)/oxdk.mk + +# The XBE cannot link until theseuslib.lib exists. Prerequisite-only rule: +# OXDK owns the recipe. +$(OUTPUT_DIR)/$(XBE_TITLE).exe: $(THESEUSLIB_LIB) + +$(THESEUSLIB_LIB): $(THESEUSLIB_OBJS) + $(LLD_LINK) /lib /nologo /out:$@ $^ + +# theseuslib's own headers are included with angle brackets, so they need a +# real -I. The -iquote paths above deliberately do not cover them, which is +# what keeps its from resolving to theseus/engine's. +$(OUTPUT_DIR)/theseuslib/%.obj: $(THESEUSLIB_DIR)/src/%.c | $(OUTPUT_DIR)/theseuslib + $(CLANG) $(OXDK_CFLAGS) $(CFLAGS) -I$(THESEUSLIB_DIR)/include -o $@ $< + +$(OUTPUT_DIR)/theseuslib: + @mkdir -p $@ + +# minimp3 compiles in isolation: no XDK headers, no xdk_compat.h force-include, +# and no stdcall default. It only wants the CRT headers. Overriding both flag +# variables for this one object reproduces that, and it is why the +# _mp3dec_*@N alternatenames above exist -- its symbols stay cdecl while every +# caller is stdcall. +MINIMP3_OBJ := $(call oxdk_obj,$(THESEUS_SRC)/engine/minimp3_decode.cpp) +$(MINIMP3_OBJ): OXDK_CXXFLAGS := $(OXDK_TARGET_FLAGS) -c $(OXDK_OPT) +$(MINIMP3_OBJ): CXXFLAGS := -MMD -MP -fno-rtti \ + -I$(THESEUS_SRC)/engine -I$(XDK_DIR)/include -Wno-typedef-redefinition + +# Experiment: HOT_O2=1 builds only the per-draw render files at -O2 and leaves +# the rest at -O0. If it boots and holds, the -O2 crash lives elsewhere; use a +# separate BUILDS_ROOT so it never mixes with the -O0 objects. +ifdef HOT_O2 +HOT_O2_OBJS := $(foreach f,render/text.cpp render/widget_draw.cpp render/asset_loader.cpp \ + render/shape_render.cpp render/xip_archive.cpp,$(call oxdk_obj,$(THESEUS_SRC)/$(f))) +$(HOT_O2_OBJS): OXDK_OPT := -O2 +endif + +# Present as the Xbox Dashboard: same title ID, version and keys as the +# retail XBE in DASH_IDENTITY_XBE. Re-applied whenever default.xbe is rebuilt. +ifneq ($(strip $(DASH_IDENTITY_XBE)),) +IDENTITY_STAMP := $(OUTPUT_DIR)/.identity-applied +all: $(IDENTITY_STAMP) +$(IDENTITY_STAMP): $(OUTPUT_DIR)/default.xbe $(DASH_IDENTITY_XBE) xbe_identity.py + python3 xbe_identity.py --from "$(DASH_IDENTITY_XBE)" --to $< + @touch $@ +endif + +-include $(THESEUSLIB_DEPS) +-include $(OBJS:.obj=.d) diff --git a/build/mk-appimage.sh b/build/mk-appimage.sh index 0fbc46a..d5a8b8f 100755 --- a/build/mk-appimage.sh +++ b/build/mk-appimage.sh @@ -9,7 +9,8 @@ set -euo pipefail SRC="${1:-$HOME/builds/theseus/desktop}" REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -OUT="${2:-$REPO/UIX-Desktop-x86_64.AppImage}" +ARCH="$(uname -m)" +OUT="${2:-$REPO/UIX-Desktop-$ARCH.AppImage}" APP="$REPO/AppDir" command -v patchelf >/dev/null || { echo "need patchelf"; exit 1; } @@ -86,9 +87,9 @@ echo "AppDir: $(ls "$APP/usr/lib" | wc -l) bundled libs, $(du -sh "$APP" | cut - APPIMAGETOOL="${APPIMAGETOOL:-}" if [ -z "$APPIMAGETOOL" ]; then APPIMAGETOOL=/tmp/appimagetool - wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O "$APPIMAGETOOL" || - wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O "$APPIMAGETOOL" + wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage -O "$APPIMAGETOOL" || + wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$ARCH.AppImage -O "$APPIMAGETOOL" chmod +x "$APPIMAGETOOL" fi -APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGETOOL" "$APP" "$OUT" +ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGETOOL" "$APP" "$OUT" echo "built: $OUT ($(du -h "$OUT" | cut -f1))" diff --git a/build/xbe_identity.py b/build/xbe_identity.py new file mode 100644 index 0000000..5875470 --- /dev/null +++ b/build/xbe_identity.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Give a built XBE the certificate identity of a retail dashboard XBE. + +The online library logs on as the running XBE's certificate, and T:\\ / U:\\ +follow its title ID, so a replacement dashboard has to present the same +identity as the one it replaces. This copies every identity field from the +retail XBE you point it at into the target, in place. No keys live here; +they're read from your own retail XBE at build time. Keys are never printed. +""" + +import argparse +import struct +import sys + +# Certificate field offsets (see cxbe's Xbe.h). +FIELDS = [ + ("title id", 0x008, 4), + ("title name", 0x00C, 0x50), + ("alternate title ids", 0x05C, 0x40), + ("allowed media", 0x09C, 4), + ("game region", 0x0A0, 4), + ("game ratings", 0x0A4, 4), + ("disk number", 0x0A8, 4), + ("version", 0x0AC, 4), + ("lan key", 0x0B0, 16), + ("signature key", 0x0C0, 16), + ("alternate signature keys", 0x0D0, 0x100), +] +CERT_MIN = 0x1D0 + + +def cert_offset(buf, what): + if buf[:4] != b"XBEH": + sys.exit(f"{what}: not an XBE") + base, = struct.unpack_from(" {describe(dst, do)}, keys copied") + + +if __name__ == "__main__": + main() diff --git a/build/xbox-sources.mk b/build/xbox-sources.mk new file mode 100644 index 0000000..d176adc --- /dev/null +++ b/build/xbox-sources.mk @@ -0,0 +1,89 @@ +# Xbox source lists. Shared by build/Makefile and build/Makefile.xbox so the +# two never drift. Paths are relative to $(THESEUS_SRC). + +SRCS_ENGINE = \ + engine/array.cpp \ + engine/debug.cpp \ + engine/dotfield_node.cpp \ + engine/locale_node.cpp \ + engine/math_node.cpp \ + engine/settings_nodes.cpp \ + engine/xap_compile.cpp \ + engine/minimp3_decode.cpp + +# Shared sources compiled for BOTH Xbox and Desktop +SRCS_SHARED = \ + shared/activefile.cpp \ + shared/date_node.cpp \ + shared/file_ops.cpp \ + shared/file_util.cpp \ + shared/keyboard_node.cpp \ + shared/music_collection.cpp \ + shared/ntiosvc.cpp \ + shared/overlay_alert.cpp \ + shared/recovery_node.cpp \ + shared/savegame_grid.cpp \ + shared/screen.cpp \ + shared/settingsfile.cpp \ + shared/string_util.cpp \ + shared/timezone.cpp \ + shared/title_collection.cpp \ + shared/util.cpp \ + shared/frame_profiler.cpp \ + shared/xap_vm.cpp \ + shared/xbe.cpp + +# Xbox-only shared files. Desktop provides replacements or stubs. +SRCS_SHARED_XBOX_ONLY = \ + shared/audio_system.cpp \ + shared/discord.cpp \ + shared/harddrive.cpp + +SRCS_RENDER = \ + render/animation_nodes.cpp \ + render/asset_loader.cpp \ + render/camera.cpp \ + render/hud_node.cpp \ + render/materials.cpp \ + render/node_system.cpp \ + render/scene_groups.cpp \ + render/shape_render.cpp \ + render/starfield.cpp \ + render/text.cpp \ + render/tmap_system.cpp \ + render/widget_draw.cpp \ + render/widget_layer.cpp \ + render/xip_archive.cpp + +SRCS_XBOX = \ + xbox/config.cpp \ + xbox/disc_management.cpp \ + xbox/dsound_manager.cpp \ + xbox/fps_widget.cpp \ + xbox/ftp_widget.cpp \ + xbox/iso_widget.cpp \ + xbox/input.cpp \ + xbox/live_accounts.cpp \ + xbox/main.cpp \ + xbox/memory_monitor.cpp \ + xbox/memutil.cpp \ + xbox/modchip.cpp \ + xbox/network.cpp \ + xbox/overlay.cpp \ + xbox/panic_screen.cpp \ + xbox/perf_widget.cpp \ + xbox/theseus_launcher.cpp \ + xbox/title_scanner.cpp \ + xbox/widget_config.cpp \ + xbox/xbox_live.cpp + +SRCS_TOOLBOX = \ + toolbox/drive.cpp \ + toolbox/driveManager.cpp \ + toolbox/fileSystem.cpp \ + toolbox/ftpServer.cpp \ + toolbox/socketUtility.cpp \ + toolbox/stringUtility.cpp \ + toolbox/utils.cpp + +ALL_SRCS = $(SRCS_ENGINE) $(SRCS_SHARED) $(SRCS_SHARED_XBOX_ONLY) $(SRCS_RENDER) $(SRCS_XBOX) $(SRCS_TOOLBOX) diff --git a/docs/README.md b/docs/README.md index 6099714..3c1895d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,41 +1,48 @@ # Theseus Documentation -This directory contains the technical documentation for Theseus: the Xbox dashboard engine reverse-engineered from the 5960 retail XBE. +Technical documentation for Theseus, the Xbox dashboard engine rebuilt from binary analysis of the retail dashboard, and for UIX Desktop, the build of it that runs on macOS, Linux and Windows. If you just want to run it, the main [README](../README.md) is the place to start. -## Contents +## Reference -### Reference +- **[XAP Script Contract](xap-contract.md)**: every node type, function, property and callback the dashboard's XAP scripts use. The "do not break" surface any implementation (Xbox, desktop, future ports) has to honor. +- **[xips-source](../xips-source/README.md)**: the editable source for the scene archives Theseus adds or changes. -- **[XAP Script Contract](xap-contract.md)**: Complete catalog of every node type, function, property, and callback that XAP scripts in the dashboard XIPs reference. The "do not break" API surface that any implementation (Xbox, desktop, future ports) must honor. +## What we found in the dashboard binary (`decomp/`) -### Binary Analysis (`decomp/`) +Per subsystem notes from analyzing the 5960 retail dashboard XBE in Ghidra. Each file documents the structures, function tables, properties and behavior identified for one part of the dashboard. [decomp/README.md](decomp/README.md) is the address by address index. -Per-subsystem reverse engineering notes derived from analysis of the retail 5960 XBE in Ghidra. Each file documents the structures, function tables, properties, and behavior identified for one subsystem of the dashboard. - -- **[VM](decomp/VM.md)**: XAP bytecode VM: opcode set, operator table, built-in functions, variable resolution -- **[Node](decomp/Node.md)**: Scene graph reflection, FND and PRD struct layouts, how the structures were identified from the binary -- **[XIP](decomp/XIP.md)**: Archive format, file layout, texture loading, mesh buffer layout +- **[VM](decomp/VM.md)**: XAP bytecode VM: opcode set, operator table, built in functions, variable resolution +- **[Compiler](decomp/Compiler.md)**: how XAP text becomes bytecode and scene nodes +- **[Node](decomp/Node.md)**: scene graph reflection, FND and PRD struct layouts, how the structures were identified in the binary +- **[XIP](decomp/XIP.md)**: archive format, file layout, texture loading, mesh buffer layout - **[AudioSystem](decomp/AudioSystem.md)**: DirectSound pipeline, CAudioBuf, CAudioPump, CFilePump, CAudioClip, CMusicCollection -- **[SceneGroups](decomp/SceneGroups.md)**: Group, Layer, Background, Level grouping nodes +- **[SceneGroups](decomp/SceneGroups.md)**: Group, Layer, Background and Level grouping nodes - **[ShapeRender](decomp/ShapeRender.md)**: Shape, Appearance, Material, Box, Sphere, falloff shading, mesh normal smoothing -- **[TmapSystem](decomp/TmapSystem.md)**: Dynamic textures, delta field warping, audio visualizer, FFT, particle dot field +- **[HudNode](decomp/HudNode.md)**: the HUD and Picture screen space overlay nodes +- **[TmapSystem](decomp/TmapSystem.md)**: dynamic textures, delta field warping, audio visualizer, FFT, particle dot field - **[AnimationNodes](decomp/AnimationNodes.md)**: TimeSensor, interpolators, Viewpoint, NavigationInfo -- **[FileOps](decomp/FileOps.md)**: File and directory copy, save game grid backend -- **[Keyboard](decomp/Keyboard.md)**: On-screen keyboard layout and input handling -- **[Camera](decomp/Camera.md)**: View matrix construction -- **[Text](decomp/Text.md)**: Font rendering, glyph layout, color formatting +- **[FileOps](decomp/FileOps.md)**: file and directory copy, save game grid backend +- **[Keyboard](decomp/Keyboard.md)**: on screen keyboard layout and input handling +- **[Camera](decomp/Camera.md)**: view matrix construction +- **[Text](decomp/Text.md)**: font rendering, glyph layout, color formatting - **[NtIoSvc](decomp/NtIoSvc.md)**: CD-ROM IOCTL service -- **[TitleCollection](decomp/TitleCollection.md)**: Saved game and title enumeration backend -- **[Settings](decomp/Settings.md)**: System settings, screen saver, INI-style config file parser -- **[Date](decomp/Date.md)** and **[Math](decomp/Math.md)**: Built-in date and math objects -- **[StringObject](decomp/StringObject.md)**: String built-in -- **[Util](decomp/Util.md)**: Utility helpers +- **[TitleCollection](decomp/TitleCollection.md)**: saved game and title enumeration backend +- **[Settings](decomp/Settings.md)**: system settings, screen saver, INI style config file parser +- **[Date](decomp/Date.md)** and **[Math](decomp/Math.md)**: built in date and math objects +- **[StringObject](decomp/StringObject.md)**: string built in +- **[Util](decomp/Util.md)**: utility helpers + +## Desktop (`desktop/`) + +How the same engine runs on macOS, Linux and Windows. The Xbox build doesn't need any of this. -### Desktop Port (`desktop/`) +- **[Building](desktop/building.md)**: every desktop target, the one time library steps, Windows cross builds, packaging +- **[Dependencies](desktop/dependencies.md)**: packages per OS, minimum library versions, Windows cross deps +- **[Desktop Features](desktop/desktop-features.md)**: games grid and launching, Title Maker, media, music, xCloud, skins, CRT, tools +- **[Porting to PC](desktop/porting-to-pc.md)**: the platform layer, drive letters and the user data folder, audio, input, nodes +- **[D3D8 to bgfx](desktop/d3d8-translation.md)**: drawing the dashboard's Direct3D 8 calls through bgfx +- **[Media Player](desktop/media-player-design.md)**: the media library, libmpv playback, and the DVD player node -Documentation specific to building Theseus for desktop platforms (macOS, Linux, in-progress Windows). The Xbox build does not need any of this. +## The Xbox build -- **[Porting to PC](desktop/porting-to-pc.md)**: How Xbox APIs were replaced with SDL2 and OpenGL -- **[D3D8 to OpenGL Translation](desktop/d3d8-translation.md)**: Rendering layer translation -- **[Desktop Features](desktop/desktop-features.md)**: Game launching, Steam, Title Maker, tools -- **[Media Player Design](desktop/media-player-design.md)**: libmpv integration with the DVD player XAP scene +Built with OXDK through `build/Makefile.xbox` (`make -f Makefile.xbox CONFIG=retail` or `CONFIG=debug` from `build/`). It needs an Xbox SDK tree of your own at `theseus/xdk`. Console work currently ships as [UIX Lite](https://github.com/OfficialTeamUIX/UIX-Lite); the Theseus XBE is a work in progress. diff --git a/docs/decomp/AnimationNodes.md b/docs/decomp/AnimationNodes.md index e328d1b..39a0164 100644 --- a/docs/decomp/AnimationNodes.md +++ b/docs/decomp/AnimationNodes.md @@ -1,34 +1,101 @@ -# Animation & Viewport Nodes +# Animation & Viewpoint Nodes -Animation and viewport nodes confirmed in the 4920-5960 retail binary. +The keyframe and binding nodes in the 5960 retail binary: `TimeSensor`, the two +interpolators, `Viewpoint` and `NavigationInfo`. All of them follow VRML97 names, but +the behavior is the dashboard's own and smaller than the spec. -## Nodes +## Registration -| Class | Address | FND String | Address | -|-------|---------|------------|---------| -| CTimeSensor | 0x000260fc | (none) | (none) | -| CPositionInterpolator | 0x0002a04c | "PositionInterpolator" | 0x0002a078 | -| COrientationInterpolator | 0x00029fcc | "OrientationInterpolator" | 0x0002a000 | -| CViewpoint | 0x00027fd4 | (none) | (none) | -| CNavigationInfo | 0x000282b0 | (none) | (none) | +| Class | Name | Size | Factory | vtable | +|-------|------|------|---------|--------| +| CTimeSensor | "TimeSensor" | 0x50 | `0x00041bf0` | `0x00026018` | +| CPositionInterpolator | "PositionInterpolator" | 0x34 | `0x0005d250` | `0x00029f70` | +| COrientationInterpolator | "OrientationInterpolator" | 0x34 | `0x0005d470` | `0x00029f10` | +| CViewpoint | "Viewpoint" | 0x48 | `0x0004f770` | `0x00027f10` | +| CNavigationInfo | "NavigationInfo" | 0x40 | `0x000504d0` | `0x000281f0` | -## CTimeSensor +## TimeSensor -Time-driven animation trigger. Properties: startTime, stopTime, loop, cycleInterval. +On top of the time dependent node (loop 0x1c, startTime 0x20, stopTime 0x24, +isActive 0x28) it adds cycleInterval 0x30, enabled 0x34, cycleTime 0x38 (double), +fraction_changed 0x40 and time 0x48 (double). The factory sets cycleInterval to 1.0 +and enabled to true; cycleTime, fraction_changed and time are left unset. -## CPositionInterpolator +**OnSetProperty** (`0x00041c70`): a write to `time` (0x48) widens the incoming float +into the double and returns 0 so the generic copy is skipped. Everything else passes. -Keyframe-based vec3 interpolation. Drives position animations between key/keyValue pairs. +**Advance** (`0x00041c90`) reads the clock first, runs the time dependent Advance +(`0x0002f0d0`), and returns unless active. Then: -## COrientationInterpolator + time = now; + if (cycleTime + cycleInterval <= now) { // x87, double precision + if (loop) cycleTime += cycleInterval; + else { isActive = 0; this->OnIsActiveChanged(); } // vtable +0x5c + } + f = (float)((now - startTime) / cycleInterval); + fraction_changed = f - (float)(int)f; + if (fraction_changed == 0.0 && startTime < now) fraction_changed = 1.0; + CallFunction(this, L"fraction_changed", 0, 0); -Keyframe-based quaternion slerp. Drives orientation animations between key/keyValue pairs. +The 1.0 case makes the final frame of a cycle report 1 instead of wrapping to 0. -## CViewpoint +**OnIsActiveChanged** (`0x00041d50`): turning on stamps `cycleTime = now`; turning off +calls the script's `OnActiveChanged`. -Camera position, orientation, and fieldOfView binding. +## Interpolators -## CNavigationInfo +Both keep `key` (a number array at 0x1c) and `keyValue` (count at 0x2c, data at +0x30). `key` is never read: lookups spread the input evenly over the keyValue entries. -Avatar size, speed, and visibility limit parameters. +**PositionInterpolator::Interpolate** (`0x0005d300`): + k = (count - 1) * key; i = (int)floor(k); frac = k - floor(k); + if (i < 0) return v[0]; + if (count - 1 <= i) return v[count - 1]; + if (0 < i && i < count - 2 && 3 < count) + D3DXVec3CatmullRom(&out, &v[i-1], &v[i], &v[i+1], &v[i+2], frac); + else + out = (v[i+1] - v[i]) * frac + v[i]; // per component + +So interior spans are Catmull-Rom splines and the two end spans are straight lines. + +**OrientationInterpolator::OnLoad** (`0x0005d520`) turns each stored VRML axis+angle +into a quaternion in place with `D3DXQuaternionRotationAxis(axis, 0.0 - angle)`. +**Interpolate** (`0x0005d5a0`) uses the same index math and clamps, and slerps between +neighbors with `D3DXQuaternionSlerp`. + +## Viewpoint + +Fields: isBound 0x1c, fieldOfView 0x20 (0.785398 from `0x0002c028`), jump 0x24 +(true), orientation 0x28 (0, 0, 1, 0), position 0x38 (0, 0, 10 from `0x0002c024`), +description 0x44. The bound viewpoint is the global at `0x0017e81c`; the projection +dirty flag is the byte at `0x0017e7f9`. + +- **Destructor** (`0x0004f830`) frees the description and clears the global if it + pointed here. +- **OnLoad** (`0x0004f860`) binds when nothing is bound yet, so the first viewpoint + a scene loads takes the camera. +- **BindViewpoint** (`0x0004f880`) walks the node's class chain looking for the + Viewpoint class object (`0x00187e88`) and binds on a match. +- **Bind** (`0x0004f8b0`): does nothing if already bound; otherwise unbinds the old + one, takes the global, builds the camera rotation with + `D3DXQuaternionRotationAxis(&orientation.xyz, 0.0 - orientation.w)`, flips the + position's z, calls the camera's `Set` with 0 seconds for `jump` or 1.0 otherwise, + and marks the projection dirty. +- **OnSetProperty** (`0x0004f940`): writing true to isBound binds; writing + fieldOfView marks the projection dirty. Always returns 1. + +## NavigationInfo + +Fields: isBound 0x1c, avatarSize 0x20 (0.25, 1.6, 0.75), headlight 0x2c (true), +speed 0x30 (1.0), type 0x34, visibilityLimit 0x38, shape 0x3c. The bound one is the +global at `0x0017e820`. + +- **Destructor** (`0x00050580`) frees type and clears the global if it pointed here. +- **OnLoad** (`0x000505b0`): if nothing is bound, takes the global, sets isBound and + marks the projection dirty. +- **OnSetProperty** (`0x000505e0`): isBound written true sets it and marks dirty; + avatarSize (0x20) and visibilityLimit (0x38) mark dirty. Returns 1. + +Avatar size and visibility limit are what the projection code turns into near and far +planes. diff --git a/docs/decomp/AssetLoader.md b/docs/decomp/AssetLoader.md new file mode 100644 index 0000000..18c58df --- /dev/null +++ b/docs/decomp/AssetLoader.md @@ -0,0 +1,217 @@ +# Asset Loader + +What we found in the Xbox Dashboard about how it gets textures and meshes on screen: the +`Texture` and `ImageTexture` nodes, a hundred slot texture cache fed by a background file +loader, the XPR texture parser, and the `Mesh` node with the `.xm` reader behind it. +Everything here comes from the 5960 retail XBE. Theseus rebuilds it in +`render/asset_loader.cpp`, one file for both builds. + +## Registration + +| Class | Name string | Size | Factory | vtable | +|-------|-------------|------|---------|--------| +| CTexture | "Texture" | 0x30 | `0x00060db0` | `0x0002a518` | +| CImageTexture | "ImageTexture" | 0x38 | `0x00060ef0` | `0x0002a4b8` | +| CMeshNode | "Mesh" | 0x40 | `0x0005c050` | `0x00029d70` | + +Texture props (PRD at `0x00175d74`): `repeatS` (bool, 0x1c), `repeatT` (bool, 0x1d). +ImageTexture (PRD at `0x00175ee0`): `url` (string, 0x30), `alpha` (bool, 0x34). +Mesh (PRD at `0x0017504c`): `url` (string, 0x1c). Its one script function is `load` +(FND entry `0x00188418`, sig 12). + +The other fields: Texture keeps its format at 0x20 (the factory writes 6, A8R8G8B8), the +surface at 0x24 and the size at 0x28/0x2c. ImageTexture adds a reload flag at 0x35 that +starts set. Mesh keeps a dirty flag at 0x20, the last render time (double) at 0x28, the +mesh at 0x30, whether it owns that mesh at 0x34, and a link to the next mesh node at 0x38 +(list head `0x0017c8d4`). + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x00060990 | ParseTexture | File bytes to a texture | +| 0x00060c90 | LoadTexture | Forced `.xbx` lookup in the XIPs | +| 0x00060d40 | CreateTexture | One level managed texture, reports the size it got | +| 0x00060ec0 | CTexture::Create | Texture vtable slot 23 | +| 0x00060f90 | CImageTexture::GetTextureSurface | ImageTexture slot 22 | +| 0x00060fc0 / 0x00061030 | CBackgroundLoader ctor / dtor | | +| 0x00061090 | CBackgroundLoader::Fetch | Starts the read | +| 0x000611f0 | CBackgroundTexture dtor | Clears the slot's loading flag | +| 0x00061210 | CBackgroundTexture::OnComplete | Loader vtable `0x0002a4ac` slot 1 | +| 0x00061290 | CleanupTextureCache | Frees the oldest texture | +| 0x000614f0 | UnloadTexture | Drops every slot holding a url; only the MOTD clear calls it | +| 0x000615a0 | FindTexture | The cache | +| 0x00061760 | CImageTexture::Load | Surface from the cache | +| 0x00061820 | CImageTexture::OnSetProperty | | +| 0x0005bd00 | CMesh dtor | | +| 0x0005bda0 | CMesh::GetFVF | Slot 1; the same three bytes serve `CStrObject::length` | +| 0x0005bdb0 | CMesh::Create | Reads a `.xm` file | +| 0x0005bea0 | CMesh::Render | Slot 0 | +| 0x0005c1f0 | CMeshNode::Init | Clears dirty | +| 0x0005c200 | CMeshNode::Render | | +| 0x0005c250 | CMeshNode::load | | +| 0x0005c320 | LoadMesh | New CMesh from a path | +| 0x0005c390 | MakeSphere | Used by `Sphere` (`0x00052aa0`) | + +Several Ghidra labels in the project are off by one function here: `CBackgroundTexture_OnComplete` +is ParseTexture, `GetTexture` is `CImageTexture::Load`, `CreateMesh_File` is LoadMesh. + +## The texture cache + +A flat array of one hundred 0x20 byte slots at `0x0017da70`: url, texture, last used time +(double), a loading flag at 0x10, and the time the texture arrived (double) at 0x18. A pointer +at `0x0017da68` marks the slot being filled so a memory cleanup triggered by that very load +leaves it alone. + +``` +FindTexture(url, async, width, height, inXIP): // 0x000615a0 + for each slot: + if (slot.url && !wcsicmp(slot.url, url)) { slot.lastUsed = now; return slot; } + if (!free && !slot.loading) + if (!slot.url) free = slot; + else if (!old || slot.lastUsed < old.lastUsed) old = slot; + if (!free && old) { free(old.url); Release(old.texture); zero old; free = old; } + locked = free; free.url = copy of url; free.lastUsed = now; + if (!async || inXIP) { free.texture = LoadTexture(url); free.timeLoaded = now; } + else { free.loading = 1; loader = new CBackgroundTexture(free, width, height); + if (!loader->Fetch(url)) delete loader; } + locked = NULL; return free; +``` + +`CleanupTextureCache` runs the same oldest search but skips the locked slot and anything +loading, frees one, and says whether it did. It's the first thing every out of memory retry +in the dashboard tries; `CleanupMeshCache` is second, and a third failure prints +`xdash: out of memory (failed to allocate %d bytes)` and reboots to the firmware. The ten +slot unrolled loop is the compiler's. + +The title image material (`CIconMatInfo_Setup`, `0x000572d0`) calls FindTexture with async +set and fades the image in from `timeLoaded`. `ImageTexture` goes through `CImageTexture::Load` +(`0x00061760`): release the old surface, `FindTexture(url, 0, 0, 0, 0)`, `AddRef` the cached +texture and read its level 0 size. + +`0x000614f0` walks all hundred slots, frees and zeroes every one whose url matches, and +returns whether it found any. Its only caller is the MOTD clear (`0x0006a590`), which +deletes the cached `t:\motd` files and drops the `.xbx` one from the cache. + +## The background loader + +`CBackgroundLoader` is 0x2c bytes: vtable, next, file handle, an `OVERLAPPED` at 0x0c, the +buffer at 0x20, its size at 0x24, the absolute url at 0x28. The constructor appends it to +the tail of the list at `0x0017e6f0`. + +``` +Fetch(url): // 0x00061090 + MakeAbsoluteURL(abs, url); this->url = copy of abs; + flags = OVERLAPPED | SEQUENTIAL_SCAN, plus NO_BUFFERING when url starts c: or y:; + file = CreateFile(abs, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, flags, 0); + if (file == INVALID) return 0; + size = (GetFileSize(file) + 511) & ~511; // whole sectors for unbuffered I/O + data = new BYTE[size]; overlapped = 0; + if (ReadFile(file, data, size, 0, &overlapped)) { OnComplete(); delete this; } + else if (GetLastError() != ERROR_IO_PENDING) return 0; + return 1; +``` + +Nothing polls inside the loader. The frame loop (`0x0002da00`), right after the lerpers +advance, walks the list and for every loader whose `overlapped.Internal` is no longer +`STATUS_PENDING` (0x103) calls `OnComplete` and deletes it. + +`CBackgroundTexture` adds the cache slot (0x2c) and the size the image must be (0x30/0x34). +OnComplete runs ParseTexture on the buffer; when that fails and the width asked for was 64 +it loads `xboxlogo64.xbx` instead, for 128 `xboxlogo128.xbx`. Then it clears the slot's +loading flag and stamps `timeLoaded`. The destructor clears the loading flag too, so a +failed Fetch leaves the slot usable. + +## ParseTexture (`0x00060990`) + +Decides by the url's extension: + +- `xt`: the first 0x14 bytes are a texture header, the rest is pixel data. +- `xbx` with `XPR0` magic: the header sits after the 12 byte XPR header, the pixels at + `dwHeaderSize`. It refuses a header region under 0x14 bytes, a resource that isn't a + texture (`Common & 0x70000 != 0x40000`), an empty pixel block, or one that runs past the + file. When the caller gave a width and height, the format's USIZE and VSIZE fields + (`>> 20 & 0xf`, `>> 24 & 0xf`) must encode exactly those, through a table of powers of two + that reads 0 past 4096. +- Anything else, or an `.xbx` without the magic: `D3DXCreateTextureFromFileInMemoryEx` with + default size, one mip level, unknown format, managed pool, retrying through the cache + cleanups on `E_OUTOFMEMORY`. + +Both XPR branches copy the header into a 0x14 byte noncontiguous block and the pixels into +128 byte aligned contiguous memory, point the header at the pixels (physical address unless +it's a vertex buffer, what `Register` does) and set `D3DCOMMON_D3DCREATED`. + +LoadTexture (`0x00060c90`) is only the XIP half: make the url absolute, replace whatever +extension it has with `xbx`, `FindObjectInXIP(path, TEXTURE)`. + +## Meshes + +A `.xm` file is a 0x18 byte header (primitive type, faces, FVF, stride, vertices, indices), +the vertices, then 16 bit indices. The types are the PC D3D numbers: 4 becomes the Xbox +triangle list, 5 the strip, anything else makes `Create` fail. None of the three reads is +checked. The buffers are built by hand (`0x0003d410`, `0x0003d390`, see [XIP](XIP.md)). + +`LoadMesh` (`0x0005c320`) is `new CMesh`, `CreateFile(path, GENERIC_READ, 0, 0, OPEN_EXISTING, +SEQUENTIAL_SCAN)`, and `Create` if it opened. It returns the mesh either way. + +``` +CMesh::Render(bSetFVF): // 0x0005bea0 + if (!vb || !ib) return; + if (bSetFVF) SetVertexShader(GetFixedFunctionShader(fvf)); + if (faces > 800 && !g_bEdgeAntialiasOverride) { EDGEANTIALIAS = 0; MULTISAMPLEANTIALIAS = 1; } + if (g_uMesh == 0) { // 0x00179f5c, zeroed every frame + save the device view (pDevice + 0xc60); + view = inverse(affine(camera rotation 0x00187fe0, position 0x00187fd4)), third row negated; + } + SetStreamSource(0, vb, stride); SetIndices(ib, 0); + DrawIndexedVertices(type, the index count for that many faces, indices); + if (g_uMesh == 0) put the saved view back; + g_uMesh++; +``` + +So the first loose mesh drawn each frame gets the camera's own view, without the noisy +wobble, and every later one the device's current view. + +``` +CMeshNode::load(file): // 0x0005c250 + MakeAbsoluteURL(abs, file); + mesh = FindObjectInXIP(abs, any); + if (mesh) { ownMesh = 0; return; } + ownMesh = 1; mesh = LoadMesh(abs); + if (!mesh->GetFVF()) { delete mesh; MakePath(abs, xipDir, file); mesh = LoadMesh(abs); } +``` + +Render stamps the time, loads on first use (the inlined `Initialize`: load if the url is +set, then `Init`), and returns if there's still no mesh. Otherwise it draws with +`bSetFVF = (g_pRenderMeshNode != this)`: the shape renderer sets the FVF itself for the +node it's drawing. The destructor `free`s an owned mesh without running its destructor, +because the base class has no virtual one. + +MakeSphere (`0x0005c390`) asks D3DX for a sphere, clones it to XYZ, normal and diffuse +(stride 0x1c), recomputes normals and takes the buffers. On any failure it releases the +buffers, deletes the mesh and carries on with a NULL mesh into the repack below. Then it +repacks each vertex into 0x14 bytes, position, `CompressNormal` (`0x0005c820`) and diffuse, +and switches the FVF to XYZ, NORMPACKED3, DIFFUSE. + +## Where Theseus differs + +- `LoadTexture` first tries the active skin folder under every name in the asset's + equivalence group (`render/skin_assets.h`), then the XIPs, then the file itself + (`.tga`, `.bmp`, `.dds` through D3DX on Xbox). A miss raises an alert. +- `CMesh::Load` and `CMeshNode::load` try the skin folder before the XIPs the same way. +- Texture has a `titleImage` prop and ImageTexture a `fromfile` prop. Every live + ImageTexture is on a list so a skin switch can drop and reload them. +- `FlushTextureCache` and `FlushMeshCache` throw everything out for a skin switch, and + `PreloadSkinTextures` loads a skin's `.xbx` files ahead of time. +- The background loader also turns off buffering for the e:, f:, g:, r: and s: partitions. +- The first mesh of a frame is drawn with the device view like any other; Theseus's camera + loads the device view every frame. +- A loose mesh that isn't beside the scene is looked for beside the app, not in the XIP folder. +- A texture header whose pixel allocation fails is freed and the load returns NULL. +- Desktop decodes textures in software: XPR files through `xbx_texture.h`, `.jpg`, `.png` + and `.bmp` through stb_image, anything else is refused. Loose files decode from the bytes + already fetched. It only probes the skin folder for files a skin can replace, and when the + XIP misses a texture it tries the other names in its equivalence group there too. +- Desktop has no edge AA swap, adds a `falloff` prop to Mesh, counts drawn vertices, names + each texture for the inspector, and scans the skin folder through the host layer. +- No out of memory reboot: allocations retry through `NewFailed` and give up. diff --git a/docs/decomp/Camera.md b/docs/decomp/Camera.md index d86a94b..4e2dd11 100644 --- a/docs/decomp/Camera.md +++ b/docs/decomp/Camera.md @@ -1,20 +1,92 @@ -# Camera System +# Camera -Camera nodes confirmed in the 4920-5960 retail binary. +The 5960 retail dashboard has exactly one camera. It's a `Camera` node object sitting in +.data at `0x00187fb0`, and everything that moves the view (viewpoints, camera paths, +the noisy wobble) goes through it. `CameraPath` is the node that flies it along a pair +of interpolators. No shipping XAP uses `CameraPath`; level arrivals start one from code. -## Nodes +## Registration -| Class | Address | FND String | Address | -|-------|---------|------------|---------| -| CCamera | 0x00028420 | "Camera" | 0x00028430 | -| CCameraPath | 0x000383d4 | "CameraPath" | 0x000383ec | +| Class | Name string | Size | Factory | vtable | +|-------|-------------|------|---------|--------| +| CCamera | "Camera" | 0x90 | `0x00050640` | `0x00028350` | +| CCameraPath | "CameraPath" | 0x38 | `0x00050a70` | `0x000282f0` | -## CCamera +Camera props: `mode` (string, 0x1c), `lookat` (node, 0x20). Neither is read anywhere +in the binary. CameraPath props: `isActive` (0x20), `backward` (0x21), `interval` +(0x1c), `position` (0x24), `orientation` (0x28). -Scene camera with position and orientation interpolation. Uses quaternion slerp for smooth orientation transitions. +## Camera fields -The "camera" global is accessible from XAP scripts for binding and controlling the active view. +| Offset | What | +|--------|------| +| 0x24 | position (vec3) | +| 0x30 | orientation (quaternion) | +| 0x40 / 0x4c | position and orientation when the last move started | +| 0x5c / 0x68 | where the move is going | +| 0x78 | move start time (double, from the global clock at `0x0017e808`) | +| 0x80 | move duration in seconds, 0 when idle | +| 0x84 | the position the last view matrix was built from | -## CCameraPath +The factory sets the duration to 0, position to (0, 0, 20), orientation to identity +(the 1.0 at `0x0002bed8` in w), mode to NULL, and also clears the noisy camera flag +at `0x0017c614`. Lookat is left as whatever the allocator returned. The destructor +(`0x00050760`) frees `mode` and runs the node destructor. -Spline-based camera animation path. Drives the level transition fly-through sequences between dashboard panels. +## Set (`0x000508e0`) + +Copies the target into the "going to" slots, copies the current pose into the "came +from" slots, stamps the start time and stores the duration. A duration of zero or +less snaps position and orientation straight to the target. + +## Advance (`0x000509a0`) + +If a move is running, `f = (now - start) / duration`. At `f >= 1.0` the duration is +zeroed and `f` held at 1. Position is the straight line `old + (new - old) * f` +(written out per component) and orientation is `D3DXQuaternionSlerp`. Then it always +calls UpdateViewMatrix. It does not call the node Advance. + +## UpdateViewMatrix (`0x00050780`) + +Works on copies of position and orientation. When the noisy flag is set it builds a +small wobble: `t = now * 0.25` (double at `0x0002c178`), yaw `sin(t * PI/2)` and pitch +`sin(t * PI)`, each times one degree in radians (`0x0002c174`), roll 0, and multiplies +it onto the orientation copy. The constants mean the head drifts at most one degree. + +It stores the position at 0x84, then + + D3DXMatrixAffineTransformation(&g_matPosition /*0x17f4a8*/, 1.0, NULL, &rot, &pos); + D3DXMatrixInverse(&g_matView /*0x17f468*/, NULL, &g_matPosition); + g_matView._31.._34 = 0.0 - each; // SUBSS from a zeroed register + +copies the 16 floats into the device's view transform at `pDevice + 0xc60`, and ORs +`0xff0200` into the dirty mask at `0x00156080`. Negating the third row is what turns +the scene's right handed coordinates into D3D's left handed view. + +## CameraPath + +The factory sets interval 1.0, both flags false and both node pointers NULL. + +**Advance** (`0x00050b60`) runs the node Advance, then does nothing unless +`isActive`. Otherwise `t = (now - start) / interval`; at `t >= 1` it clamps to 1 and +clears `isActive`. With `backward` set, `t = 1 - t`. It starts from the camera's +current pose, replaces the position with `CPositionInterpolator::Interpolate(t)` +(`0x0005d300`) with z negated, replaces the orientation with +`COrientationInterpolator::Interpolate(t)` (`0x0005d5a0`) when those nodes are set, +and calls `Set` with a zero duration so the camera snaps there every frame. + +**OnSetProperty** (`0x00050c90`) restamps the start time whenever the field at 0x20 +(`isActive`) is written, and lets the write through. + +**Destructor** (`0x00050af0`) releases `position` through vtable slot 4 but only +nulls `orientation` without releasing it. + +The level code starts a path by setting `isActive`, the start time and `backward` +inline (see [SceneGroups.md](SceneGroups.md)). + +## Theseus notes + +- The camera's constructor keeps the noisy flag the config loader set; the binary's + factory clears it. +- Theseus routes the view matrix through `TheseusSetTransform` instead of writing the + device block directly. diff --git a/docs/decomp/Compiler.md b/docs/decomp/Compiler.md new file mode 100644 index 0000000..d59f982 --- /dev/null +++ b/docs/decomp/Compiler.md @@ -0,0 +1,258 @@ +# Compiler (xap_compile.cpp) + +How the 5960 dashboard turns XAP text into bytecode. The compiler is three +layers: a character scanner, a statement and expression compiler that emits +VM ops for script functions, and a scene compiler that emits node +construction ops for a class body. All text is UTF-16 in the retail XBE. +Opcode names are listed in VM.md. + +## Globals + +| Address | Meaning | +|---------|---------| +| 0x0017ae90 | current file name (shown in syntax errors) | +| 0x0017ae94 | current line number | +| 0x0017ac80 | parse error flag (byte) | +| 0x0017ac88 | buffer the `#file` directive copies a name into | +| 0x0017ae98 | class namespace list head (`Class_Init` / `Class_Exit`) | +| 0x000201c4, 0x00021b98 | empty strings returned on a hard parse failure | + +`CClass::ParseFile` (0x00033d00) sets file = name, line = 1, error = 0, +compiles the body, reports `"Stuff past expected end of file!"` if anything +is left, then clears file and line and returns `error == 0`. + +## Scanner + +| Function | Address | +|----------|---------| +| SyntaxError (free) | 0x00032cf0 | +| SkipWhite | 0x00032d70 | +| Token | 0x00032f70 | +| ParseBoolean | 0x00033150 | +| ParseInteger | 0x000331d0 | +| ParseNumber | 0x00033250 | +| ParseString | 0x000332d0 | +| ParseVec3 / ParseVec4 | 0x00033340 / 0x00033370 | +| ParseIntArray | 0x000333a0 | +| ParseVecArray | 0x00033490 | +| ExpandCString | 0x00033690 | + +**SyntaxError** formats the message into a 512 character buffer, wraps it +with the file name and line into a 1024 character report, and sets the error +flag. The retail build never displays the report. + +**SkipWhite(pch, mask)** eats every character up to and including space, +counting newlines. The mask enables `#` (1), `//` (2) and `/* */` (4) +comments; every caller passes 0xffffffff. A `#` line may be a directive: + +```c +// after '#' and any spaces/tabs +if (!wcsncmp(p, L"line", 4)) line = _wtol(p + 4 + blanks) - 1; +else if (!wcsncmp(p, L"file", 4)) { copy optional-quoted name to 0x17ac88; file = 0x17ac88; } +// then skip to end of line either way +``` + +**Token** returns the next token and its length, in one of four shapes: + +- quoted (`"` or `'`): the text between the quotes, `\` protects the next + character, the closing quote is consumed but not part of the token; +- path mode (third argument set, used by `DEF` and `USE`): everything up to + the next blank; +- identifier: starts with a letter or `_`, continues with letters, digits, `_`; +- otherwise a word of letters, digits, `.`, `-`, `_` if it starts with one of + those, or an operator run of anything else up to a blank or word character. + +**ParseInteger / ParseNumber** copy the token into a 64 character stack +buffer with no length check, then `_wtol` / `wcstod`. **ParseBoolean** uses +`wcsnicmp` against `true` then `false` over the token length. **ParseString** +copies the token unexpanded into a new buffer. + +**ParseIntArray / ParseVecArray** require `[`, remember the cursor and line, +and walk the list twice: once to count (entries split by an optional `,`, +each entry `nVecSize` numbers), then allocate `count * 4` bytes, rewind the +cursor and line, and read for real. ParseVecArray inlines the number read. +Neither loop checks for the end of the text, so an unterminated array never +finishes. + +**ExpandCString(out, max, tok, cch)** expands `\b \f \n \r \t`, `\xHH`, +`\uHHHH`, octal `\ooo` (and a bare `\0`), and passes any other escaped +character through. Errors: `"string constant is too long"`, +`"backslash at end of string"`, `"Invalid hex character escape sequence"`, +`"Invalid hex character"`, and the two Unicode equivalents. + +## Code buffer (CCompiler) + +| Function | Address | +|----------|---------| +| Write(pv, cb) | 0x00034440 | +| Write(BYTE) | 0x00034490 | +| WriteInteger | 0x000344c0 | +| WriteString | 0x000344f0 | +| GrowTo | 0x00034590 | +| SyntaxError (member) | 0x000345e0 | +| CreateFunction | 0x00034670 | +| ~CCompiler | 0x000346f0 | + +Layout: `+4` op count, `+8` op buffer, `+0xc` buffer size, `+0x10` error +byte. GrowTo rounds the request up to a 4 KB page, copies and frees the old +buffer. The writers are inlined at most call sites. WriteString writes the +character count as a dword, then the characters. CreateFunction allocates +`0x1c + ops` bytes, builds a CFunction (vtable 0x00022c18, type 13) and +copies the ops in after the header. The member SyntaxError also sets the +compiler's own error byte. + +## Script compiler (CFunctionCompiler) + +| Function | Address | +|----------|---------| +| ParseTerm | 0x00034710 | +| ParseOperator | 0x00034bb0 | +| ParseArray | 0x00034c20 | +| ParseCall | 0x00034c90 | +| ParseExpression(pch, prio) | 0x00034d80 | +| ParseIF | 0x00034ea0 | +| ParseWHILE | 0x00034fe0 | +| ParseDO | 0x00035170 | +| ParseFOR | 0x000352f0 | +| FixupBreaks | 0x00035510 | +| ParseBREAK | 0x00035540 | +| ParseCONTINUE | 0x00035610 | +| ParseRETURN | 0x000356c0 | +| ParseSLEEP | 0x00035760 | +| ParseBlock | 0x00035800 | +| ParseStatement | 0x00035960 | +| ParseLocalVar | 0x00035bf0 | + +Layout past the CCompiler base: 100 local slots of `{short len; ptr name}` +at `+0x14`, frame size at `+0x334`, break list at `+0x338`, loop depth at +`+0x33c`, loop top address at `+0x340`, behavior flag at `+0x344`. Nothing +checks the 100 slot limit. + +### Operators + +The table at 0x000200e8 holds 22 ten-byte entries `{ch1, ch2, ch3, priority, +opcode}`, scanned in order with the first match winning: + +| Text | Priority | Op | +|------|----------|----| +| `.` | 1 | 0x0d | +| `*` `/` | 2 | 0x0b 0x0c | +| `+` `-` | 3 | 0x09 0x0a | +| `<<` `>>` | 4 | 0x17 0x18 | +| `<=` `>=` `<` `>` | 5 | 0x14 0x15 0x12 0x13 | +| `==` `!=` | 6 | 0x10 0x11 | +| `&` | 7 | 0x1a | +| `^` | 8 | 0x1c | +| `\|` | 9 | 0x1b | +| `=` `+=` `-=` `*=` `/=` `%=` | 13 | 0x0e 0x20 0x21 0x22 0x23 0x24 | + +Because `+`, `-`, `*` and `/` come before their compound forms, `a += 1` +scans as `+` and the compound entries are never reached. There are no `&&` +or `||` entries; scripts use `&` and `|`. + +ParseExpression is precedence climbing from 14 down to 0, left associative. +Level 0 is ParseTerm; at level 1 a `(` after the operand is a call +(`opCall` + one byte argument count) and a `[` is an index (`opArray`). + +### Terms + +ParseTerm folds a run of unary `-`, `+` and `~` (`~` only reports +`"Sorry, the ~ operator has not been implemented yet!"`), then: + +- `( expr )`; +- a quoted string: `opStr` + expanded text (1024 character buffer); +- a token starting with `.`, `-` or a digit: copied into a 256 character + buffer (no length check), `wcstod`, a pending minus folded into the + constant, `opNum` + float; +- `true` / `false`: `opNum` 1.0 / 0.0; `this`: `opThis`; `null`: `opNull`; +- `new Name(args)`: the args, then `opNew` + count + name; +- a declared local: `opLocal` + slot; anything else: `opVar` + name. + +An unfolded minus emits `opNeg` after the term. + +### Statements + +ParseStatement leaves a `}` alone, sends `{` to ParseBlock, and otherwise +emits `opStatement` + the current line before dispatching on the keyword +(compared by length): `var`, `for`, `if`, `do`, `while`, `break`, `continue`, +`return`, and `sleep` in behaviors only. Anything else is an expression +statement compiled from the token start, then `opDrop` and a `;` that may be +missing at the end of the text. + +- **Block**: `opFrame` + local count (patched), statements, `opEndFrame` + + count; the frame size drops back afterwards. +- **if**: `opCond` + exit, then; `else` adds `opJump` over the else part. + Without `else` the cursor backs up to the start of the peeked token. +- **while**: top at the condition, `opCond` + exit, body, `opJump` top. +- **do**: steps one character past `do`, top at the body, then + `while ( cond ) ;` emits `opCond` + exit and `opJump` top. +- **for**: init (`var` is read from three characters past the `(`), top at + the condition, the step clause compiled once to find `)` and discarded, + body, step again + `opDrop`, `opJump` top. +- **break** pushes a forward `opJump` on the break list, patched to the loop + end by FixupBreaks; **continue** jumps to the loop top. Both report + `"unexpected 'break'"` / `"unexpected 'continue'"` outside a loop. +- **return** / **sleep**: optional value (`opNull` if none), then `opRet` / + `opSleep`. +- **var a = x, b;**: each name takes the next slot; an initializer emits + `opLocal` slot, the value, `opAssign`, `opDrop`. + +## Scene compiler (CClassCompiler) + +| Function | Address | +|----------|---------| +| CClass::ParseClassBody | 0x00033d60 | +| Compile (node list) | 0x00035e50 | +| ParseNode | 0x00035ec0 | +| ParseChildren | 0x000363c0 | +| ParseProps | 0x00036440 | +| ParseMemberVar | 0x00036ad0 | +| ParseFunction | 0x00036c30 | +| ParseClass | 0x00036e50 | +| Class_Init / Class_Exit | 0x00036f00 / 0x00036f30 | + +The target class sits at `+0x14`. ParseClassBody compiles a node list until +`}` or the end, appends a 0 byte, and stores the CFunction in the class. + +**ParseNode**, by first token: + +- `DEF name` (path token): new CMember (vtable 0x00022dd8) in the next + variable slot (class `+0x68`), `opDefNode` + slot, then the node; +- `USE name`: `opUseNode` + slot of an existing member, else + `"Undefined USE '%s'"` (name cut to 255 characters); +- `behavior {...}` and `function name(...) {...}`: compiled and set as class + members, a function named `behavior` is refused; +- `var`, `class`, `import Name, url` (url copied into a stack buffer with no + length check, the imported class loaded and defined by name); +- otherwise a script class or built-in node class by name (`"Unknown class: + %s"`): `opNewNode` + name, optional `{ props }` (`"Exected a '}'"`, sic), + `opEndNode`. + +One trailing `,` is consumed after every item. + +**ParseProps** reads names until `}`. Inline `behavior` / `function` +compile into the class instance function array (`+0x58`) and emit +`opFunction` + name + index. Other names go through the node class property +table (`"Unknown property: %s"`), and the 12-byte PRD `{offset, type, name}` +drives the encoding: + +| Type | Reader | Emitted | +|------|--------|---------| +| children (4), node array (11) | ParseChildren | `opInitArray` offset, nodes, `opEndArray` | +| node (9) | ParseNode | `opNewNodeProp` PRD, node | +| integer (1) | ParseInteger | `opInitProp` PRD, 4, value | +| boolean (2) | ParseBoolean | `opInitProp` PRD, 1, value | +| number (3) | ParseNumber | `opInitProp` PRD, 4, value | +| vec3 (5), color (8) | ParseVec3 | `opInitProp` PRD, 12, values | +| vec4 (6), quaternion (7) | ParseVec4 | `opInitProp` PRD, 16, values | +| string (10) | ParseString | `opInitProp` PRD, length, characters | +| int array (12) | ParseIntArray | `opInitProp` PRD, bytes, values | +| number / vec2 / vec3 / vec4 array (13 to 16) | ParseVecArray(1 to 4) | `opInitProp` PRD, bytes, values | + +**ParseChildren** takes one node or a `[ ... ]` list. **ParseMemberVar** +adds members and refuses initializers (`"Sorry, class member initializers +have not been implemented yet!"`). **ParseFunction** builds a +CFunctionCompiler on the stack, reads the parameter list into the local +slots (behaviors have none), requires a `{` body, and appends `opNull opRet` +before creating the function. **ParseClass** compiles a nested class body +into a new CClass and defines it in the class namespace. diff --git a/docs/decomp/Date.md b/docs/decomp/Date.md index 768752a..790d4c8 100644 --- a/docs/decomp/Date.md +++ b/docs/decomp/Date.md @@ -40,3 +40,21 @@ Same pattern without the `FileTimeToLocalFileTime` step. Accepts 0 args (current time) or 3-7 args (year, month, day, [hour, min, sec, ms]). Two-digit years get +1900. Month is 0-based (JS convention), converted to 1-based for SYSTEMTIME. + +## Function addresses + +| Address | Name | What it does | +|---------|------|--------------| +| 0x00030f90 | isLeapYear | 100/400 exception first, then `year % 4` | +| 0x00030fd0 | getDaysInMonth | February from the leap test, other months from the short table at `0x001765c6` (index 0 is 0, then 31, 0, 31, 30, ...) | +| 0x00031040 | CDateObject::CDateObject | Arguments as above; each one is dereferenced to a fixed point and read through `ToNum` | +| 0x00031290..0x000313e0 | getDate .. getSeconds | `FileTimeToLocalFileTime`, `FileTimeToSystemTime`, one field (`getMonth` subtracts 1) | +| 0x00031410..0x000314f0 | getUTCDate .. getUTCSeconds | `FileTimeToSystemTime`, one field | +| 0x00031510 | toGMTString | `%02d %s %d %02d:%02d:%02d GMT` (format at `0x00021d60`) with day, month name, year, hours, minutes, seconds in UTC | +| 0x000315c0 | toLocaleString | Local time through `FormatTime` into 32 characters | +| 0x00031660 | toUTCString | The GMT string ending in `UTC` (format at `0x00021d24`) | +| 0x00031710 | SetSystemClock | See above | + +The month names come from a pointer table at `0x0017442c` indexed by the 1 based month (slot 0 is +null, slot 1 is `Jan`). The FND table is at `0x0017f698` in the order the script names are listed +there. diff --git a/docs/decomp/FileUtil.md b/docs/decomp/FileUtil.md new file mode 100644 index 0000000..34fe084 --- /dev/null +++ b/docs/decomp/FileUtil.md @@ -0,0 +1,67 @@ +# File Utilities + +The path and file helpers every part of the 5960 retail binary leans on: joining a folder and a +name, resolving script URLs against the current folder, turning a URL into a file path, and +counting blocks. Theseus's version is `theseus/shared/file_util.cpp`, compiled by both builds. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0002def0 | (create file) | `CreateFileA` on a narrowed path; the helpers below open files through it | +| 0x0002df60 | DoesFileExist | Narrows the path into 260 chars and returns whether `GetFileAttributesExA` succeeds | +| 0x0002dfc0 | MakePath | Copies the folder (unless it is already the output), adds `\` when the last character isn't one, appends the name | +| 0x0002e010 | CleanFilePath | Skips a leading `file:` (`_wcsnicmp`, 5), turns `/` into `\`, stops at `?`, narrows each character | +| 0x0002e060 | MakeAbsoluteURL2 | Resolves a URL against a base | +| 0x0002e170 | UpdateCurDirFromFile | Resolves a URL and keeps it as the current folder when absolute | +| 0x0002e200 | MakeAbsoluteURL | Current folder against the app folder, then the URL against that | +| 0x0002e260 | FindFilePath | The resolved path if a file is there, else app folder + name | +| 0x0002e2d0 | GetDirectoryBlocks | Recursive block count with a cancel event | +| 0x0002e520 | GetFileSize | Size of a file opened with all share flags and backup semantics, 0 if it won't open | + +`0x0017a480` holds the current folder and `0x0017e828` points at the app folder. + +## MakeAbsoluteURL2 + +Registers: `EAX` is the URL, `ECX` the base, `ESI` the output. + +``` +if (wcschr(url, ':') || (url[0] == '\\' && url[1] == '\\') || !base || + (!wcschr(base, ':') && !wcschr(base, '/'))) + { wcscpy(out, url); return; } +wcscpy(out, base); +p = wcsrchr(out, '/'); if (!p) p = wcsrchr(out, '\\'); +while (url starts with "../" or "..\\") { + url += 3; *p = 0; + p = last '/' or '\\' in out; if none { p = out + wcslen(out) - 1; break; } +} +*p = '/'; wcscpy(p + 1, url); +``` + +A base with a drive but no separator at all (`Q:`) leaves `p` null, and the binary writes through +it. Theseus puts the URL over the last character in that case, as it does when `../` runs out of +folders. + +## GetDirectoryBlocks + +Returns -1 straight away when the cancel event is signaled. Builds `\*.*`, narrows it and calls +`FindFirstFileA`. When that fails the result is 1 if `GetLastError()` is `ERROR_FILE_NOT_FOUND` (an +empty folder still takes its own block) and 0 otherwise. The count starts at 1 for the folder; each +file adds `(size + block - 1) / block`; with recursion on, each subfolder whose name doesn't start +with `.` adds its own count. The cancel event is checked before every entry and stops the walk with +-1. The handle is closed with `CloseHandle`. + +## CActiveFile + +A file pulled into memory by URL: URL at +4, content at +8, size at +0xc, write time at +0x10 and +an in-XIP flag at +0x18. + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0002e560 | Reset | Frees the URL and the content (`LocalFree`) and zeroes size and time | +| 0x0002e5a0 | Fetch | Resets and copies the URL. Empty succeeds with nothing. `string:` (any case) makes the rest the content. A relative URL is resolved with MakeAbsoluteURL and tried in the XIPs; failing that, with the search flag set and no such file, it becomes app folder + URL. Then the XIPs again with the final URL, then FetchFile | +| 0x0002e750 | FetchFile | Opens for sequential reading; with the try flag set, gives up unless size + 1 MB of physical memory is free; reads it all, writes two zero bytes after it and takes the write time | +| 0x0002e840 | MakeUnicode | Content starting FF FE has the mark moved off; anything else is widened byte by byte (stopping at a zero byte), terminated, and the size becomes `(bytes + 1) * 2` | + +Theseus also counts a host path starting with `/` as absolute on desktop, closes the file on the +memory check, and frees the new buffer when GetFileTime fails (5960 frees the old content there). diff --git a/docs/decomp/HudNode.md b/docs/decomp/HudNode.md new file mode 100644 index 0000000..4420449 --- /dev/null +++ b/docs/decomp/HudNode.md @@ -0,0 +1,170 @@ +# HUD & Picture + +The two screen-space overlay nodes in the 5960 retail binary. `HUD` is a group that +flattens its children onto the screen; `Picture` is a single textured (or flat +colored) quad positioned in pixels. In the shipping XAPs only `dvd.xap` uses `HUD`, +and nothing uses `Picture`. + +## Registration + +Both classes are registered from the static init code at `0x000cd010` and +`0x000cd040`. + +| Class | Name string | Class object | Size | Factory | Base | Prop map | vtable | +|-------|-------------|--------------|------|---------|------|----------|--------| +| CHud | "HUD" `0x0002891c` | `0x00188148` | 0x44 | `0x00052ae0` | Group `0x00187c68` | `0x00175190` | `0x00028870` | +| CPicture | "Picture" `0x00028900` | `0x00188174` | 0x78 | `0x00052db0` | Node `0x0017f528` | `0x001751a8` | `0x00028810` | + +Virtual slots used below: 0 scalar deleting dtor, 1 GetPropMap, 2 OnSetProperty, +4 Release, 6 GetNodeClass, 14 OnLoad, 15 Advance, 16 Render, 22 GetTextureSurface. + +The HUD prop map has no entries of its own and chains to Group. The Picture map, +in table order: + +| Offset | Type | Name | +|--------|------|------| +| 0x20 | number | top | +| 0x1c | number | left | +| 0x24 | number | width | +| 0x28 | number | height | +| 0x2c | node | texture | +| 0x30 | number | transparency | +| 0x34 | number | fade | +| 0x38 | color | color | + +Neither class has a function table, so neither exposes script methods. + +## CHud + +The factory (`0x00052ae0`) is the Group constructor inlined: zero children, bbox +center 0, bbox size -1 on every axis, both bbox flags set, then the HUD vtable. The +destructor (`0x00052b80`) only resets the vtable and runs Group's. + +**Render (`0x00052b90`).** Every device call is inlined. Decoded, in order: + +```c +D3DXMatrixOrthoLH(&ortho, 720.0, 480.0, -10000.0, 10000.0); +savedProj = g_matProjection /* 0x0017f4e8 */; +g_matProjection = ortho; SetTransform(PROJECTION, &g_matProjection); // 0x00144e30 +savedView = g_matView /* 0x0017f468 */; +g_matView = g_matIdentity /* 0x00176338 */; SetTransform(VIEW, ...); // inline copy to device +0xc60 +lighting = rs[LIGHTING] /* 0x0017a218 */; rs[LIGHTING] = 0; +worldStack->Push(); worldStack->LoadIdentity(); // stack 0x0017e7fc, slots 4, 5 +SetTransform(WORLD, worldStack->GetTop()); FUN_0005ca00(); // falloff shader constants +rs[EDGEANTIALIAS] = 0; rs[MULTISAMPLEANTIALIAS] = 1; +CGroup::Render(); // 0x0004d7e0 +worldStack->Pop(); SetTransform(WORLD, worldStack->GetTop()); FUN_0005ca00(); +rs[LIGHTING] = lighting; +g_matProjection = savedProj; SetTransform(PROJECTION, ...); +g_matView = savedView; SetTransform(VIEW, ...); +``` + +The ortho box is a fixed 720x480, so HUD children are laid out in that space no +matter the video mode. + +## CPicture + +| Offset | Field | +|--------|-------| +| 0x1c..0x28 | left, top, width, height | +| 0x2c | texture node | +| 0x30 | transparency | +| 0x34 | fade (seconds) | +| 0x38..0x40 | color | +| 0x44 | alpha | +| 0x48 | fade start time (double) | +| 0x50..0x6c | start/end pairs for left, top, width, height | +| 0x70 | vertex buffer | + +**Factory (`0x00052db0`).** left = top = 0, width = height = -1.0 (`0x0002bef0`), +texture = 0, transparency = fade = 0, color = (1, 1, 1), alpha = 1.0, fade start = +0.0, vertex buffer = 0. The start/end pairs are not touched. + +**Destructor (`0x00052e70`).** Releases the vertex buffer, then calls Release on +the texture node. + +**Render (`0x00052f10`).** + +```c +if (alpha <= 0) return; +surface = 0; +if (texture && (surface = texture->GetTextureSurface()) == 0) return; +if (!vb) vb = CreateVertexBuffer(0x90); // inline XMemAlloc, 4 x 0x24 bytes +w = width; h = height; +if (w < 0 || h < 0) { + if (!surface) return; + GetLevelDesc(surface, &desc); // 0x0014bec0 + if (w < 0) w = desc.Width; if (h < 0) h = desc.Height; + if (texture is a CTexture /* class 0x001885f4 */) { + w = (texture->imageWidth / w) * w; // fields +0x28, +0x2c + h = (texture->imageHeight / h) * h; } } +u = v = 1.0; +if (texture is a CTexture) { GetLevelDesc(surface, &desc); + u = imageWidth / desc.Width; v = imageHeight / desc.Height; } +SetTexture(0, surface); // 0x00145160 +quad[0] = { left+w, top+h, color 0xffffffff, u, v }; +quad[1] = { left, top+h, color 0xffffffff, 0, v }; +quad[2] = { left+w, top, color 0xffffffff, u, 0 }; +quad[3] = { left, top, color 0xffffffff, 0, 0 }; +for each corner { // view size at 0x0017e7f0 / 0x0017e7f4 + x -= viewWidth * 0.5; y = 0.0 - (y - viewHeight * 0.5); + z = 0; normal = (x, y, 1.0); } +rs[ALPHABLENDENABLE] = 1; rs[SRCBLEND] = 0x302; rs[DESTBLEND] = 0x303; +rs[TEXTUREFACTOR] = ARGB(alpha*255, r*255, g*255, b*255); // 255.0 at 0x0002bf38, ftol each +if (surface) { COLOROP = MODULATE(TEXTURE, TFACTOR); ALPHAOP = MODULATE(TEXTURE, TFACTOR); + MINFILTER = MAGFILTER = POINT; } +else { COLOROP = SELECTARG1(TFACTOR); ALPHAOP = SELECTARG1(TFACTOR); } +rs[EDGEANTIALIAS] = 0; rs[MULTISAMPLEANTIALIAS] = 1; +lock vb (wait on the GPU if busy); copy 0x24 dwords; +SetStreamSource(0, vb, 0x24); SetVertexShader(0x152); // XYZ | NORMAL | DIFFUSE | TEX1 +DrawPrimitive(TRIANGLESTRIP, 0, 2); +SetTexture(0, NULL); rs[ALPHABLENDENABLE] = 0; +MINFILTER = MAGFILTER = LINEAR; +``` + +The vertex buffer create and lock are never checked. The two filter states go back +to linear even on the untextured path, where they were never changed. + +**Advance (`0x000534c0`).** + +```c +CNode::Advance(s); +if (texture) texture->Advance(s); +if (fade != 0 && fadeStart != 0.0) { + t = (float)((now /* 0x0017e808 */ - fadeStart) / fade); // x87, rounded once at the end + if (t > 1.0) { fadeStart = 0.0; t = 1.0; } + left = startLeft * (1 - t) + endLeft * t; + top = startTop * (1 - t) + endTop * t; + width = startWidth * (1 - t) + endWidth * t; + height = startHeight * (1 - t) + endHeight * t; } +``` + +Alpha isn't animated here; transparency fades go through a lerper instead. + +**OnLoad (`0x000535c0`).** alpha = 1.0 - transparency, each start and end value = +the current left/top/width/height, then CNode::OnLoad. + +**OnSetProperty (`0x00053620`).** Keyed on the PRD offset: + +```c +case 0x30 /* transparency */: + a = 1.0 - value; + if (fade == 0) alpha = a; + else new CLerper(this, &alpha, a, fade); // 0x28 bytes, linked at the head of 0x0017aeac + return 1; +case 0x34 /* fade */: + if (value == 0) fadeStart = 0.0; + CLerper::RemoveObject(this); + return 1; +case 0x1c /* left */: endLeft = value; startLeft = left; break; +case 0x20 /* top */: endTop = value; startTop = top; break; +case 0x24 /* width */: endWidth = value; startWidth = width; break; +case 0x28 /* height */:endHeight = value; startHeight = height; break; +default: return 1; +} +if (fade != 0) { fadeStart = now; return 0; } // veto: Advance moves it there +return 1; +``` + +A move with a fade set returns 0, so the property itself keeps its old value and +Advance walks it from start to end over `fade` seconds. diff --git a/docs/decomp/Input.md b/docs/decomp/Input.md new file mode 100644 index 0000000..41397a4 --- /dev/null +++ b/docs/decomp/Input.md @@ -0,0 +1,44 @@ +# Joystick Input + +The `Joystick` node in the 5960 retail binary: it reads the pads (and the IR remote) and turns them +into script events. Only the bound joystick (`0x0017e73c`, with the previous one at `0x0017e740`) +raises events. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x00040620 | CJoystick::CJoystick | 20 polls a second (`0x0002bf04`), remote and gamepad enabled, waiting for its first poll; binds itself when no joystick is bound | +| 0x00040750 | CJoystick::~CJoystick | Drops itself from the previous binding, hands the binding back if it held it, frees the secret key | +| 0x00040790 | BindJoystick | NULL clears both bindings; a Joystick node (class `0x00181f80`) is bound | +| 0x000407f0 | CJoystick::Bind | Current binding becomes the previous one; once polled before, rereads the devices | +| 0x00040820 | CJoystick::OnSetProperty | `isBound`, `secretKey`, `enableSecretKey`, and storing `eatSecretKey`, `enableGamepad`, `enableRemote` itself | +| 0x00040b70 | EnableGlobalInput | Sets the global input switch (`0x0017c058`) | +| 0x00041040 | CJoystick::Advance | The per frame poll, below | +| 0x00041790 | CheckSecretKeySequence | Steps the secret key with one letter, returning begin (1), verified (2) and end (4) flags | +| 0x00041800 | ProcessSecretKeySequence | OnKeyVerificationEnter, OnKeyVerificationExit, OnKeyVerified for the flags set | + +## Advance + +- A remote that went away more than its grace time ago raises OnRemoveRemote once. +- Each port's state is read. The IR remote's codes become their own events (repeating every other + report after the 8th of a held key). Pads are merged. +- A change while the screen saver is showing only wakes it. +- Stick values are `raw / 32767`, zeroed between -0.9 and 0.9. When the left stick rests the d-pad + sets the axes instead. +- A held direction fires OnMoveLeft/OnMoveRight and then OnMoveDown/OnMoveUp (secret key letters + L, R, D, U) at once, after 0.3 s (`0x0002c0b8`), then every 0.12 s (`0x0002c1f4`). Coming back to + rest fires OnMoveCenter. +- Each analog button crossing 0x20 fires its down or up event (tables at `0x00176640` and + `0x00176620`, secret key letters `ABXYbwlr`). Each digital button change fires its event (tables + at `0x00176600` and `0x001765e0`, letters at `0x000259a0`: none for the d-pad, then S, P, Q, W). + Start and Back raise OnADown/OnBDown and OnAUp/OnBUp. +- A typed USB keyboard key raises OnTIDKeyDown. +- Start with A and B down raises OnReset. +- Holding the progressive scan combo toggles progressive scan once (see the video path notes). + +Every event goes through the binding and `enableGamepad` checks (`enableRemote` for remote events). + +Theseus's desktop node reads an SDL pad and the keyboard as one player, adds media keys, has no +remote or progressive scan combo, accepts pad events even with `enableGamepad` off, and fires +OnMoveCenter after the button events rather than before them. diff --git a/docs/decomp/Keyboard.md b/docs/decomp/Keyboard.md index 731a47c..594a1e2 100644 --- a/docs/decomp/Keyboard.md +++ b/docs/decomp/Keyboard.md @@ -1,33 +1,354 @@ # On-Screen Keyboard -CKeyboard at 0x00029d38, "Keyboard" at 0x00029d4c, FND at 0x00013da4. +The `Keyboard` node in the 5960 retail binary: the grid of keys behind every name, passcode and +soundtrack prompt. The script moves a highlight around the grid and presses keys; the node keeps a +31 character edit string with a cursor and hands it to a Text node to draw. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0005ab30 | CKeyboard::GetPropMap | Returns the PRD at `0x00175448` | +| 0x0005ab40 | CKeyboard::GetFunctionMap | Returns the FND at `0x00188250` | +| 0x0005ab50 | CKeyboard::CreateNode | Factory for `Keyboard`, 0x9c bytes | +| 0x0005ab70 | CKeyboard::GetNodeClass | Returns the node class at `0x00188224` | +| 0x0005ab80 | CKeyboard::CKeyboard | Constructor | +| 0x0005ac20 | CKeyboard::~CKeyboard | Destructor | +| 0x0005acb0 | GetAsteriskFormattingState | Reads the *heading* flag | +| 0x0005acc0 | SetAsteriskFormattingState | Writes the *heading* flag | +| 0x0005ace0 | selectKey | Puts the highlight on a cell | +| 0x0005ad40 | selectUp | Highlight up | +| 0x0005adb0 | selectDown | Highlight down | +| 0x0005ae60 | selectLeft | Highlight left | +| 0x0005af50 | selectRight | Highlight right | +| 0x0005b040 | TIDactivate | Presses whatever key a USB keyboard just typed | +| 0x0005b340 | activate | Presses the highlighted key | +| 0x0005b630 | Backspace | Erases left of the cursor | +| 0x0005b6a0 | Delete | Erases under the cursor | +| 0x0005b700 | CursorLeft | Cursor left | +| 0x0005b740 | CursorRight | Cursor right | +| 0x0005b780 | Shift | Toggles shift | +| 0x0005b7b0 | CycleMode | Next page | +| 0x0005b7e0 | Insert | Inserts a string at the cursor | +| 0x0005b890 | (grid char test) | Whether a character is on any page of the current grid | +| 0x0005b970 | CKeyboard::OnSetProperty | Loads `string` into the edit buffer | +| 0x0005b9d0 | CKeyboard::Render | Frame, text, then one node per key | +| 0x0005bca0 | CKeyboard::Advance | Advances the control node too | + +## The object + +| Offset | Field | +|--------|-------| +| +0x1c | `keys`, a nodearray (length at +0x1c, entries at +0x24) | +| +0x28 | `frame` | +| +0x2c | `text` | +| +0x30 | `control` | +| +0x34 | `string` | +| +0x38 | `shift` (byte) | +| +0x39 | `caps` (byte) | +| +0x3a | `TIDKeyUsed` (byte) | +| +0x3b | `PlaySound` (byte) | +| +0x3c | `CloseKeyboard` (byte) | +| +0x40 | `mode`, the western page | +| +0x44 | `jmode`, the Japanese page | +| +0x48 | edit buffer, 32 WCHARs | +| +0x88 | edit length | +| +0x8c | cursor, -1 when the whole string is selected | +| +0x90 | highlighted row | +| +0x94 | highlighted column | +| +0x98 | a WORD key code that stands in for the grid during one `activate` (TIDactivate's) | + +The constructor (`0x0005ab80`, and the same stores inlined into CreateNode) zeroes every field +and sets the cursor to -1. The destructor releases `text`, `frame` and `control` in that order, +frees `string` and releases the nodearray. ## Properties -| Name | Type | -|------|------| -| keys | nodearray | -| frame | node | -| text | node | -| control | node | -| mode | integer | -| jmode | integer | -| shift | boolean | -| caps | boolean | -| string | string | +PRD at `0x00175448`, dumped with DumpPRD: -## Functions +| Name | Type | Offset | +|------|------|--------| +| keys | 11 (nodearray) | 0x1c | +| frame | 9 (node) | 0x28 | +| text | 9 (node) | 0x2c | +| control | 9 (node) | 0x30 | +| mode | 1 (integer) | 0x40 | +| jmode | 1 (integer) | 0x44 | +| shift | 2 (boolean) | 0x38 | +| caps | 2 (boolean) | 0x39 | +| TIDKeyUsed | 2 (boolean) | 0x3a | +| PlaySound | 2 (boolean) | 0x3b | +| CloseKeyboard | 2 (boolean) | 0x3c | +| string | 10 (string) | 0x34 | + +## Script functions + +FND at `0x00188250`: selectKey (sig 18), selectUp, selectDown, selectLeft, selectRight, activate, +TIDactivate, Backspace, Delete, CursorLeft, CursorRight, Shift, CycleMode (all sig 1), Insert +(sig 12), SetAsteriskFormattingState (sig 14), GetAsteriskFormattingState (sig 2). + +Callbacks it raises: `OnDone` and `OnError`. + +## Globals + +| Address | What | +|---------|------| +| 0x0017c024 | current language; 1 (Japanese) switches to the 9 row grid | +| 0x0017e748 | the string the next Text node draws | +| 0x00175220 | the cursor index for that Text node, -1 for none | +| 0x0017e74c | *heading* formatting flag, a BSS byte (starts off) | +| 0x0017e74d | "this key is highlighted", read by the key nodes | +| 0x0017f460 | WORD key code of the last USB keyboard keystroke | +| 0x0017f462 | WORD character of that keystroke | + +The two keystroke words belong to the input object: the poll at `0x00040520` walks the eight +keyboard handles, takes the first `XInputGetKeystroke` that succeeds, resets the screen saver and +copies the 8 byte keystroke to the object's +0x28. The joystick node's Advance (`0x00041040`) +raises `OnTIDKeyDown` when the key code is nonzero and that joystick is the bound one. + +## The grids + +Six WORD tables, 11 columns each, listed through `0x00175430` (alpha `0x000211b8`, symbols +`0x00021228`, accents `0x00021298`, hiragana `0x00021308`, katakana `0x000213d0`, Japanese +English `0x00021498`). Western grids are 5 rows, Japanese 9. Codes 1 to 14 are function keys: + +| Code | Key | Label | +|------|-----|-------| +| 1 | shift | SHIFT | +| 2 | caps lock | CAPS LOCK | +| 3 | alphabet page | ALPHABET | +| 4 | symbols page | SYMBOLS | +| 5 | accents page | ACCENTS | +| 6 | cursor left | `<` | +| 7 | cursor right | `>` | +| 8 | backspace | BACKSPACE | +| 9 | done | DONE | +| 10 | space | SPACE | +| 11 | hiragana page | HIRAGANA | +| 12 | katakana page | KATAKANA | +| 13 | English page | ENGLISH | +| 14 | blank cell | a space, never drawn | + +Western alpha page (symbols and accents swap the characters and the page keys): + +``` +DONE 1 2 3 4 5 6 7 8 9 0 +SHIFT a b c d e f g h i j +CAPS k l m n o p q r s t +ACCENTS u v w x y z BACKSPACE x4 +SYMBOLS SPACE x6 < < > > +``` + +The western bottom two rows repeat a key across cells (BACKSPACE in 4, SPACE in 6, `<` and `>` in +2 each), which is why a western keyboard has 45 key nodes for 55 cells. The Japanese grids put the +page keys, SPACE, BACKSPACE, `<`, `>` and DONE down column 10 and leave a few blank cells (code 14) +in rows 0 and 7. + +## Moving the highlight + +**selectKey (`0x0005ace0`).** + +```c +last = (lang == 1) ? 8 : 4; +row = (row < 0 || row > last) ? 0 : row; +col = (col >= 0 && col < 11) ? col : 0; +``` + +**selectUp / selectDown (`0x0005ad40`, `0x0005adb0`).** + +```c +do { row = row ? row - 1 : last; } while (grid[row*11 + col] == 14); // up +do { row = (row == last) ? 0 : row + 1; } while (grid[row*11 + col] == 14); // down +``` + +**selectLeft (`0x0005ae60`).** + +```c +for (;;) { + while (col == 0) { col = 10; if (grid[row*11 + 10] != 14) return; } + if (lang != 1) { + if (row == 3) { if (col > 6) { col = 6; return; } } + else if (row == 4) { if (col > 8) { col = 8; return; } col = (col < 7) ? 0 : 6; return; } + col--; return; + } + col--; if (grid[row*11 + col] != 14) return; +} +``` + +The row 4 step is compiled as `((col < 7) - 1) & 6`. + +**selectRight (`0x0005af50`).** + +```c +for (;;) { + while (col == 10) { col = 0; if (grid[row*11] != 14) return; } + if (lang != 1) break; + col++; if (grid[row*11 + col] != 14) return; +} +if (row == 3) col = (col < 7) ? col + 1 : 0; +else if (row == 4) col = (col >= 9) ? 0 : (col == 0) ? 1 : (col > 6) ? 9 : 7; +else col++; +``` + +## Pressing a key + +**activate (`0x0005b340`).** The key is the +0x98 code if set, otherwise `grid[row*11 + col]`. + +```c +switch (key) { +case 1: Shift(); return; +case 2: if (mode == 0 || mode == 2) { caps = !caps; return; } break; +case 3: case 4: case 5: mode = key - 3; return; +case 11: case 12: case 13: jmode = key - 11; return; +case 6: CursorLeft(); return; +case 7: CursorRight(); return; +case 8: Backspace(); return; +case 9: + for (i = 0; i < len && buf[i] == ' '; i++) ; + if (i == len) break; // empty or all blanks + for (p = &buf[len-1]; *p == ' '; p--) *p = 0; + for (q = buf; *q == ' '; q++) ; + if (q != buf) memmove(buf, q, (wcslen(q) + 1) * 2); + free(string); string = new WCHAR[len + 1]; + copy len WCHARs; string[len] = 0; + CallFunction(this, L"OnDone"); return; +case 10: + if (cursor == -1) { len = 0; cursor = 0; } + if (len > 30) return; // full: no error + open a gap at cursor; buf[cursor++] = ' '; len++; return; +default: + if (cursor == -1) { len = 0; cursor = 0; } + if (len < 31) { + open a gap at cursor; + buf[cursor++] = (caps != shift) ? CharUpperW(key) : key; + shift = 0; len++; return; + } +} +CallFunction(this, L"OnError"); +``` + +DONE copies `len` characters even after trimming, so trailing spaces arrive in `string` as +nulls and leading ones as whatever sat past the shifted text's terminator. SPACE does not clear +shift, and a key that bounces off a full string leaves shift on. + +## Editing + +Each editing function opens the same way: a cursor of -1 means the whole string is selected, +so the edit starts from an empty string with the cursor at 0. + +**Backspace (`0x0005b630`).** + +```c +if (cursor == -1) { len = 0; cursor = 0; } +if (cursor > 0) { memmove(&buf[cursor-1], &buf[cursor], (len - cursor) * 2); len--; cursor--; } +else CallFunction(this, L"OnError"); +``` + +**Delete (`0x0005b6a0`).** After the selection check, if `cursor < len` it steps the cursor +right one (an inlined CursorRight, whose -1 case can't happen any more) and calls Backspace; +otherwise OnError. + +**CursorLeft / CursorRight (`0x0005b700`, `0x0005b740`).** From -1 they land on 0 and on `len` +respectively; otherwise they step one, raising OnError at either end. + +**Shift (`0x0005b780`).** OnError unless `mode` is 0 or 2, else toggles `shift`. + +**CycleMode (`0x0005b7b0`).** `mode++` and `jmode++`, each reset to 0 once above 2. + +**Insert (`0x0005b7e0`).** + +```c +if (cursor == -1) { len = 0; cursor = 0; } +n = wcslen(s); +if (len + n > 31) { CallFunction(this, L"OnError"); return; } +memmove(&buf[cursor + n], &buf[cursor], (len - cursor) * 2); +copy n WCHARs to &buf[cursor]; cursor += n; len += n; shift = 0; +``` + +**Get/SetAsteriskFormattingState (`0x0005acb0`, `0x0005acc0`).** Read and write the global +byte at `0x0017e74c` (`param != 0`). The flag isn't per keyboard; the Text node reads it. + +## Node hooks + +**OnSetProperty (`0x0005b970`).** Only the `string` offset (0x34) does anything: + +```c +s = *value ? *value : L""; +n = wcslen(s); if (n > 31) n = 31; +copy n WCHARs to buf; len = n; cursor = -1; +return 1; +``` + +**Advance (`0x0005bca0`).** `CNode::Advance`, then `control->Advance` if there is one. + +**Render (`0x0005b9d0`).** -selectKey, selectUp, selectDown, selectLeft, selectRight, activate, Backspace, Delete, CursorLeft, CursorRight, Shift, CycleMode, Insert. +```c +grid = (lang == 1) ? japanese[jmode] : western[mode]; +if (frame) frame->Render(); +if (text) { buf[len] = 0; g_textCursor = cursor; g_text = buf; text->Render(); g_textCursor = -1; } +if (lang == 1) { if (keys.count != 99) return; cells = 99; } +else { if (keys.count != 45) return; cells = 55; } +node = 0; +for (i = 0; i < cells; i++) { + if (lang != 1 && i > 0 && grid[i] == grid[i-1]) continue; + k = keys.Get(node); // bounds checked, null past the end + hi = row*11 + col; + if (lang != 1) { // a wide key lights from any of its cells + if (41 <= hi && hi <= 43) hi = 40; + else if (46 <= hi && hi <= 50) hi = 45; + else if (hi == 52) hi = 51; + else if (hi == 54) hi = 53; + } + g_text = label for grid[i]; // the table above, or the character itself, + // CharUpperW'd on western grids when caps != shift + g_activeKey = (i == hi); + if (grid[i] != 14) k->Render(); + node++; +} +``` -## Script Callbacks +## TIDactivate -OnDone, OnError. +`0x0005b040` turns a USB keyboard keystroke into a key press. The keyboard XIPs call it from +`OnTIDKeyDown`, then read `CloseKeyboard`, `PlaySound` and `TIDKeyUsed` to pick between closing, +the key click and the error sound. -## Layout +```c +CloseKeyboard = 0; PlaySound = 1; TIDKeyUsed = 1; code = 0; +switch (keycode) { // 0x0017f460 +case 0x35: case 0x88: case 0x90: PlaySound = 0; break; +case 0x4f: code = 7; break; // right arrow: cursor right +case 0x50: code = 6; break; // left arrow: cursor left +} +if (code == 0) { + switch (ch) { // 0x0017f462 + case 0: break; + case 8: code = 8; break; // backspace + case 10: code = 9; break; // enter: DONE + case 0x1b: CloseKeyboard = 1; break; // escape + case 0x20: code = 10; break; // space + default: + if (grid char test(ch)) { + code = ch; + page = first of the current language's three grids with a cell equal to ch + or to CharUpperW of the cell; + jmode = page; // western grids write jmode too, not mode + } + } +} +if (code) { activate(); code = 0; TIDKeyUsed = 1; } +else { TIDKeyUsed = 0; } +``` -Three western modes: alpha, symbol, accent, arranged in a 5x11 grid. +`code` is the +0x98 field. Both switches are jump tables: the key code one covers 0x35..0x90, +the character one 0..0x20, and everything else falls to the default. The key codes are USB HID +usages (0x4f right arrow, 0x50 left arrow; 0x35, 0x88 and 0x90 are the grave key and the Japanese +input mode keys, which press nothing and stay quiet). -Three Japanese modes: hiragana, katakana, English, arranged in a 9x11 grid. +**Grid char test (`0x0005b890`).** Takes the character in DI. Returns false for 1 to 14 (the +function key codes). Otherwise walks the three grids of the current language (55 or 99 cells +each) and returns true on the first cell equal to the character or to `CharUpperW` of the cell. -Maximum input length is 31 characters. +A typed character goes in through activate's default case, so it is still uppercased again when +caps and shift disagree, and it still clears shift. diff --git a/docs/decomp/Locale.md b/docs/decomp/Locale.md new file mode 100644 index 0000000..9802e5c --- /dev/null +++ b/docs/decomp/Locale.md @@ -0,0 +1,139 @@ +# Locale and the Translator Node + +The 5960 retail binary keeps one translation table for the current language, a `Translator` node +that scripts call into, and the region and language aware date, time and number formats. This is +what the binary does; Theseus's version lives in `theseus/engine/locale_node.cpp` and both builds +compile it. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0003c370 | LoadXlate | Loads a language section and parses it into the table | +| 0x0003c620 | Translate | Copies a string, then swaps it for its translation if the table has it | +| 0x0003c680 | CTranslator::GetFunctionMap | Returns the FND at `0x00180408` | +| 0x0003c690 | CTranslator::CreateNode | Factory for `Translator`, a bare 0x1c byte CNode | +| 0x0003c6d0 | CTranslator::GetNodeClass | Returns the node class at `0x001803d8` | +| 0x0003c700 | CTranslator::~CTranslator | Straight to CNode's destructor | +| 0x0003c710 | GetTimeZoneCount | Returns 75 | +| 0x0003c720 | GetTimeZoneText | Translated display name of a zone, empty string out of range | +| 0x0003c780 | GetTimeZoneIndex | Best zone for a `TIME_ZONE_INFORMATION` | +| 0x0003c820 | GetTimeZoneInfo | Fills a `TIME_ZONE_INFORMATION` from the zone table | +| 0x0003c8f0 | SetLanguage | Switches the table to another language | +| 0x0003c980 | Translate (script) | `Translate` into a 1024 character buffer, as a string object | +| 0x0003ca10 | TranslateStripColon | The same, minus one trailing `:` | +| 0x0003cac0 | GetLanguageCode | Two letter code for the current language | +| 0x0003cb00 | GetLanguageCode (script) | The same as a string object | +| 0x0003cbc0 | GetDateSeparator | `.`, `-` or `/` by region and language | +| 0x0003cc40 | GetDateSeparator (script) | The same as a one character string | +| 0x0003ccb0 | FormatNumber | `swprintf(buf, L"%d", n)` as a string object | +| 0x0003cd40 | FormatDate | The date half of FormatTime | +| 0x0003ce90 | FormatTime | Date then time, laid out for the locale | + +## State + +| Address | What | +|---------|------| +| 0x0017aee8 | The table: 550 (key, value) pointer pairs, 0x44c dwords, zeroed on every language change | +| 0x0017c018 | The loaded section's data | +| 0x0017c01c | The section handle | +| 0x0017c020 | Current region | +| 0x0017c024 | Current language | +| 0x001745b0 | Section name per language, nine pointers (`EnglishXlate` first) | + +A locale is `region << 16 | language`. The regions the binary tests are 1 (North America), 2 (Japan) +and 4 (rest of world). Languages run English 0, Japanese 1, German 2, French 3, Spanish 4, +Italian 5, Korean 6, Chinese 7, Portuguese 8. + +## Loading a language + +`SetLanguage` (0x0003c8f0) does nothing when the language is already loaded (same number and a +non-empty first key) or out of `0..8`. Otherwise it stores the language, frees the old section with +`XFreeSectionByHandle`, zeroes the table and calls `LoadXlate` with the section name. Afterwards +it walks the nine title arrays at `0x00182088` (stride 0x944) and resets each loaded one with +`CTitleArray::SetRoot(0)`, so title names are fetched again in the new language. + +`LoadXlate` (0x0003c370) calls `XGetSectionHandleA` and `XLoadSectionByHandle`. A failed load +frees the texture cache, then the mesh cache, and retries; when neither frees anything it prints +"xdash: out of memory" and returns to the firmware. The data must start with `FEFF`, otherwise it +also returns to the firmware (code 4). + +The text is UTF-16, one entry per CR ended line: + +``` +[blanks] [# comment] CR blank or comment line, skipped +[blanks] key [blanks] = [blanks] value [blanks] [# comment] CR +``` + +- A key in quotes runs to the closing quote or the line end. A bare key runs to the first + character at or below space, or `=`; inside a bare key `_` is rewritten to `:` when the next + character is `=` or space, and to a space otherwise. +- The value may be quoted the same way and runs to a quote or the line end. It goes through + `ExpandCString` in place, so `\n`, `\x`, `\u` escapes work. +- A comment on a line of its own ends at CR or LF; a comment after a value ends at CR only. +- When the `=` or the final CR is missing the binary stops reading the file. An empty key or + value skips the line. There is no bound check on the 550 slots. + +## Translate + +``` +wcscpy(out, in); +p = wcschr(out, '@'); if (p) *p = '@'; // writes back the same character +for (i = 0; i < 550 && table[i].key; i++) + if (!wcscmp(table[i].key, out)) { wcscpy(out, table[i].value); break; } +return out; +``` + +## Separators and formats + +`GetDateSeparator` (0x0003cbc0) switches on the locale value: + +| Locale values | Separator | +|---------------|-----------| +| 0x10002, 0x10005, 0x20002, 0x20003, 0x20005, 0x40002, 0x40003, 0x40005 | `.` | +| 0x10006, 0x20006, 0x40006 | `-` | +| anything else | `/` | + +German and Italian use dots in every region, French outside North America, Korean dashes. + +`FormatDate` and `FormatTime` take a locale (`-1` means the current one). The date part: + +| Locale | Format | Fields | +|--------|--------|--------| +| 0x10000 | `%02d%c%02d%c%d ` | month, day, year | +| regions 1, 2, 4 with language 1, 6 or 7 | `%d%c%02d%c%02d ` | year, month, day | +| anything else | `%02d%c%02d%c%d ` | day, month, year | + +The time part in `FormatTime` is `%d:%02d %s` on a 12 hour clock for languages 0, 1, 6 and 7 in +regions 1, 2 and 4 (hours above 11 drop 12, 0 becomes 12, `AM` at `0x0002425c` below 12, `PM` at +`0x00024264` otherwise); everything else gets `%d:%02d` on 24 hours. Both parts go into 32 +character buffers. If the caller's size is less than their lengths plus 2 the call returns false +and writes nothing. + +Numbers have no separators: `FormatNumber` prints `%d` (format string at `0x000242b0`). The memory +monitor's block counts (0x0004ca90) print the same way and cap at `50000+`. + +`GetLanguageCode` reads two characters from `L"ENJADEFRESITKOTWBR"` for languages 0 to 9 (9 runs +past the last code) and gives an empty string otherwise. + +## Time zones + +The zone table at `0x000201f0` has 75 records of 0x1c bytes: display name, DST flag, bias, standard +bias, daylight bias, standard name, standard change date (month, day, weekday, hour as bytes), +daylight name (may be null) and daylight change date. + +`GetTimeZoneIndex` (0x0003c780) scores each zone whose bias matches: 1 for the bias alone, 2 when +the standard and daylight biases match too, and returns at once when the first 4 characters of the +standard name also match (the EEPROM keeps only 4). Otherwise the first zone with the best score +wins, or -1. + +`GetTimeZoneInfo` (0x0003c820) zeroes all 0xac bytes, copies the three biases, the standard name +and the daylight name (the standard name when there is none) with `wcsncpy(..., 32)`, and the month, +day, weekday and hour of each change date. + +## Theseus differences + +- The language text comes from `Q:\Language\.dat` instead of an XBE section, for the first + six languages, with up to 1000 pairs and a bound check. A missing file leaves the table empty. +- Desktop strings are UTF-8, so desktop converts the UTF-16 text as it loads. +- The title arrays are emptied with `DeleteAll(false)` rather than `SetRoot(0)`. diff --git a/docs/decomp/Materials.md b/docs/decomp/Materials.md new file mode 100644 index 0000000..52ef935 --- /dev/null +++ b/docs/decomp/Materials.md @@ -0,0 +1,146 @@ +# Materials + +How the 5960 retail dashboard colors its meshes. A mesh names its material with a +`MaxMaterial` node; the name is looked up in a list of 127 material objects built by +`Material_Init` (`0x00057cb0`), and that object's one virtual, `Setup`, programs the +texture stages and render states for the draw. The full retail list, with colors, is in +`theseus/render/materials_retail.inl`. + +## Render state decoding + +Every device call in these functions is inlined. The texture stage shadow table starts at +`0x001539c0` (0x80 bytes a stage, one dword per `D3DTSS_*` value) and the render state +shadow at `0x0017a080` (one dword per `D3DRS_*` value), so for example `0x0017a2d0` is +TEXTUREFACTOR and `0x0017a2dc` / `0x0017a2e0` are EDGEANTIALIAS / MULTISAMPLEANTIALIAS. +The push buffer writers are `0x00143010` (TEXTUREFACTOR), `0x00142c40` (EDGEANTIALIAS), +`0x001433f0` (MULTISAMPLEANTIALIAS), `0x00142d20` (CULLMODE) and `0x00143790` +(TEXCOORDINDEX, stage in ESI). The "fade" below is the scene fade at `0x00175ca0`. + +## The material object + +| Offset | Field | +|--------|-------| +| 0x00 | vtable (the material type) | +| 0x04 | name | +| 0x08 | flags: 1 the mesh's own material, 2 cull none, 4 / 8 / 0x10 / 0x20 pick a mask, 0x40 no edge AA on a solid | +| 0x0c | solid types: R, G, B, A bytes. Falloff types: side color | +| 0x10 | falloff front color | +| 0x14 | falloff shader effect: 1 falloff, 2 aniso, 3 textured falloff, 4 reflect | +| 0x18 | falloff: a copy of the name | +| 0x220, 0x221 | key: bright, text | + +`Material_Init` inlines every constructor: append to `g_rgMatInfo` (`0x0017c6b8`, count at +`0x0017e754`), store name and flags, then the type's fields. Afterwards the list is sorted +with `qsort` by `wcscmp` of the names (comparator `0x00057c70`). + +## Base Setup (`0x00057650`) + +Stage 0 TEXCOORDINDEX and TEXTURETRANSFORMFLAGS go to 0. Flag 1 returns 0, which tells the +mesh to use its own material. Flag 2 sets CULLMODE to none. Otherwise returns 1. + +## The types + +| vtable | Setup | Type | What it programs | +|--------|-------|------|------------------| +| `0x00028bf4` | `0x000576b0` | Solid | stage 0 color = TFACTOR, alpha = DIFFUSE x TFACTOR; factor = (a x fade, r, g, b); edge AA unless flag 0x40; multisample off | +| `0x00028bf0` | `0x00057790` | Falloff | needs a mesh; effect shader for the mesh's FVF; stage 0 off; edge AA on; side and front go to two vertex shader constants | +| `0x00028be8` | `0x00057940` | FalloffTex | effect 3; stage 0 color and alpha = DIFFUSE x TEXTURE; stage 1 off | +| `0x00028be0` | `0x00057af0` | Aniso | Falloff with effect 2 and no edge AA writes | +| `0x00028b6c` | `0x00056610` | InnerWall | FalloffTex plus wrap on U and V and cull none | +| `0x00028be4` | `0x00056d90` | Reflect | effect 4; constants (229, 229, 229, 0) / (229, 229, 229, 255); `SetReflectShaderFrameValues` (`0x0005c8d0`); color = TEXTURE x DIFFUSE, alpha = DIFFUSE | +| `0x00028bec` | `0x00056c70` | ModulateTexture | color = TEXTURE, alpha = TFACTOR; factor alpha pulses (below); edge AA on | +| `0x00028bf8` | `0x00056ea0` | Backing | mask on stage 0; color = TFACTOR, alpha = TEXTURE x TFACTOR | +| `0x00028bdc` | `0x00056940` | MaskTexture | mask on stage 1, which multiplies alpha and adds color; stage 2 multiplies alpha by the factor (fade x 255) | +| `0x00028bd0` | `0x000567d0` | FreeSpace | the memory gauge, below | +| `0x00028bd8` | `0x000572d0` | Icon | title images, below | +| `0x00028bcc` | `0x00057120` | Key | keyboard keys, below | +| `0x00028b68` | `0x00056f80` | EggGlowPulse | below | +| `0x00028b60` | `0x00057000` | EggGlow | below | +| `0x00028bd4` | `0x00056a50` | OrangeEggGlow | below | + +The mask for Backing and MaskTexture is the first of flags 4, 8, 0x10, 0x20 that's set: +plain, edge, main edge, AV. With none set the stage keeps whatever texture it had. + +**The pulse.** ModulateTexture, EggGlowPulse and the Live keys share a clock at +`0x0017c8c0` and remember which material last restarted it (`0x0017e750`). The level is +`1 - |sin((now - start) * 0.5 * PI)|`, one dip every two seconds. ModulateTexture restarts +the clock when a new material takes it and the fade is exactly 1, and multiplies its alpha +by `level * 0.25 + 0.75`. EggGlowPulse resets the start to 2.0 instead of now when it takes +the clock, uses side (178, 208, 0, 0) and front (255, 255, 128) with alpha +`level * 128 + 64`, then runs the Falloff Setup. + +**EggGlow.** Script writes 1 into the MaxMaterial's `param` to fire it. Setup sees the +exact 1.0 (compared as raw bits), stores the time instead, and while `param > 0` fades +`t = (now - param) * 0.8`; at `t >= 1` param goes back to 0. Front is (254, 255, 188) with +alpha `(1 - t) * 255`, so the glow lasts 1.25 seconds. The constructor shifts its +arguments one slot, so the object's flags hold `0xE4FEFFBC` and its side is 0; Setup +overwrites the front every frame, so only the side and flags show the slip. + +**Key.** The byte at `0x0017e74d` picks the palette. Clear: bright keys side +(243, 255, 107, 255) front (25, 225, 0, 34), other keys side (243, 255, 107, 222) front +(20, 192, 0, 15); text keys draw TFACTOR (190, 250, 94) with alpha fade x 178. Set (the +Live look): side (178, 208, 0, 255) front (178, 208, 0, 190), text factor black with alpha +fade x 192, and the pulse clock is taken when the fade is above 0. Both end with edge AA +on and the Falloff Setup. + +**Icon.** Uses the current title image (`0x0017c8cc`) unless it's the selection icon, +which uses the selected one (`0x0017c8c8`). The stock logos load from the XIP: +`xboxlogo64.xbx`, `xboxlogo128.xbx` and `xboxlogo64a.xbx` for the current image, +`xboxlogo64.xbx`, `xboxlogo128.xbx`, `XboxLiveAccount64.tga` and `XboxLiveAccount128.tga` +for the selected one. With a texture: cull none, no Z write, alpha blend +SRCALPHA / INVSRCALPHA, the image on stage 0 (TEXCOORDINDEX and transform flags 0), +alpha = TEXTURE x TFACTOR, and the factor alpha fades in over a quarter second +(`(now - loaded) * 4`, capped at 1) when the icon fades in. Without one the factor is 0 and +the quad draws clear. When the byte at 0x0f is set, stage 1 gets the edge mask so the icon +has soft edges; every icon in the retail list has it except `MotdIcon`. It never runs the +base Setup. + +**FreeSpace.** Stage 0 color = TEXTURE, alpha = TEXTURE x TFACTOR with factor +(fade x 70, 255, 255, 255). The texture transform is identity with +`_32 = ((1 - param) * 2 - 1) * 0.3 + 0.1`, flags COUNT2 and V clamped, so `param` slides the +gauge texture along the bar. Stage 1 color off. Returns 1 without the base Setup. + +**OrangeEggGlow.** Sets the FVF `0x10102`, builds a texture matrix from the inverse of the +current view times a scale (`0x0002c19c`, `0x0002c198`) with an `fmod` animated offset, +and generates stage 0 coordinates from camera space position (TEXCOORDINDEX `0x20000`, +COUNT3). Stage 0 modulates diffuse with the texture, stage 1 multiplies alpha by the factor +(fade alpha), stage 2 off; edge AA on. + +## Mask textures + +`GenerateRadialAlphaMask` (`0x00056110`) writes a 32 bit alpha only circle: per pixel +`d = sqrt(dx^2 + dy^2)` with dx, dy running -1..1 over the drawn block, +`a = clamp((1 - clamp(d, 0, 1)) * factor, 0, max)` and alpha `(int)(a * 255)`. Only the top +left `width * scale` by `height * scale` block is drawn and the rest is cleared. The four +builders (`0x000562d0`, `0x000563a0`, `0x00056470`, `0x00056540`) each make one 256 by 256 +A8R8G8B8 texture on first use, render the mask into a temporary buffer and swizzle it in: + +| Builder | factor, max, scale | Used by | +|---------|--------------------|---------| +| plain | 1, 1, 1 | flag 4 | +| AV | 1, 1, 0.6875 | flag 0x20 | +| edge | 10, 0.8, 1 | flag 8 and every icon | +| main edge | 3, 1, 1 | flag 0x10 | + +## MaxMaterial + +0xa8 bytes, factory `0x00059ff0`, vtable `0x00028b70`: the Material fields, then name +(0x90), param (0x98, double) and the bound material object (0xa0). + +- **OnSetProperty** (`0x0005a0f0`): a new name drops the binding so the next Render looks it + up again; a param write widens the script's float into the double itself. +- **Render** (`0x0005a130`): binds by `bsearch` on first use (comparator `0x00057c90`) and + draws nothing when the name isn't in the list. Then edge AA off and multisample on, the + Tubes override flag (`0x0017c8d0`) set for "Tubes", "TubesFade", "TubesQ" and "Tube", and + the material's Setup. +- **Destructor** (`0x0005a0d0`) frees the name. + +## Theseus notes + +- The colors come from `materials_retail.inl` and the skin, which can override any listed + material; Key, EggGlow and EggGlowPulse take their colors from the skin too (the retail + values reproduce the constants above). +- `orangeEggGlow` is a plain solid color; the desktop renderer has no camera space + texture coordinate generation for its projection yet. +- Desktop draws the AV mask at full size. +- The skin editor's color access (`MatInfo_ColorCount` and friends) is a Theseus addition. diff --git a/docs/decomp/Math.md b/docs/decomp/Math.md index fe551ca..f1b9ee9 100644 --- a/docs/decomp/Math.md +++ b/docs/decomp/Math.md @@ -33,3 +33,10 @@ Function names found in the binary string table: - All math wrappers are thin CRT calls (fabsf, sinf, cosf, etc.) - `atan2(y, x)` passes args reversed to match JS convention - `itoa` converts int to Unicode string via CRT `_itoa` + `Unicode()` + +## Constructor and constants + +`CMathClass::CMathClass` (0x00032950) registers the class as "Math" and loads E, LN2, LN10, LOG2E, +LOG10E, PI, SQRT1_2 and SQRT2 from `0x0002c034..0x0002c04c`. LOG2E and LOG10E are stored as 1.442 +and 0.434. `round` (0x00032b60) is `(int)(n + 0.5)`, `random` (0x00032b40) is `rand() / 32767`, +and `max`/`min` (0x00032af0, 0x00032b10) return the second argument on a tie. diff --git a/docs/decomp/Node.md b/docs/decomp/Node.md index 79a106f..b6ff414 100644 --- a/docs/decomp/Node.md +++ b/docs/decomp/Node.md @@ -122,3 +122,188 @@ Dynamic array of CNode pointers with reference counting. Used for `children` pro ### CTimeDepNode Timer node base class with start / stop / loop semantics. The Advance function checks `m_startTime` / `m_stopTime` against `theApp.m_now` to toggle `m_isActive`. Used by AudioClip, TimeSensor, and other time-driven nodes. + +## Node.cpp, Object.cpp and Class.cpp in the 5960 retail XBE + +The base classes sit in three runs of code, each in source-file order. + +| Range | Contents | +|---|---| +| `0x0002e8e0` to `0x0002f420` | Node.cpp: CNodeClass, CNode, PRD lookup, SetProperty, CNodeArray, CTimeDepNode, typed array SetSize | +| `0x0002f420` to `0x00030430` | Object.cpp: CObject construction, refcounting, FND lookup, Call, Dot, members | +| `0x00033970` to `0x00034430` | Class.cpp: CClass and CInstance | + +### Object layouts + +``` +CObject 0x14 bytes +0x00 vtable +0x04 m_nRefCount +0x08 m_obj + +0x0c m_parent +0x10 m_members (CNameSpace*) +CNode 0x1c bytes +0x14 m_behavior (CRunner*) +0x18 m_visible +CTimeDepNode 0x30 bytes +0x1c m_loop +0x20 m_startTime +0x24 m_stopTime + +0x28 m_isActive +0x2c m_lastStartTime +CNodeClass +0x14 m_className +0x18 m_nObjectSize + +0x1c m_pfnCreateNode +0x20 m_baseClass + +0x24 m_rgprd +0x28 m_nextClass +CClass +0x2c m_url +0x30 m_file +0x58 m_instanceFunctions + +0x64 m_constructor +0x68 m_nVarCount +CInstance 0x54 bytes CGroup up to +0x44, +0x44 m_class +0x48 m_vars +CNodeArray 0x0c bytes +0x00 m_length +0x04 m_nAlloc +0x08 m_rgpNode +``` + +Vtables: CObject `0x00021b00`, CNodeClass `0x00021958`, CNode `0x000219f8`, CTimeDepNode `0x00021990`, CInstance `0x000226a0`, CClass `0x000226fc`. Slots referenced by the code below: `+0x04` GetPropMap, `+0x08` OnSetProperty, `+0x0c` AddRef, `+0x10` Release, `+0x14` GetFunctionMap, `+0x18` GetNodeClass, `+0x1c` ToNum, `+0x20` ToStr, `+0x24` Deref, `+0x34` GetMember, `+0x38` OnLoad, `+0x5c` CTimeDepNode::OnIsActiveChanged. + +Globals: class list head `0x0017e730`, class records `0x0017f580` (Object), `0x0017f528` (Node), `0x0017f554` (TimeDepNode). PRD maps `0x00174418` (Object), `0x001743a8` (Node), `0x001743d0` (TimeDepNode). The current runner and `this` for behavior scripts live at `0x0017aea4` / `0x0017aea8`; the frame clock is the double at `0x0017e808`. + +### Function map + +| Address | Function | What it does | +|---|---|---| +| `0x0002e900` | CNodeClass::CNodeClass | Stores name, size, factory, base and PRD map, pushes the record on the front of the class list | +| `0x0002e970` | CNodeClass::~CNodeClass | Walks the list by link pointer and unlinks the record if found, then the CObject teardown | +| `0x0002ea20` | CNodeClass::FindByName | `wcsncmp` against each name plus a check that the name ends right there | +| `0x0002ea80` | CNode::~CNode | `ResetFunc` and free the behavior runner, then CObject teardown | +| `0x0002eaf0` | CNode::OnLoad | Looks up `behavior` (FindMemberFunction inlined); if found, builds a 0x1c0 byte CRunner bound to the node and points it at the function | +| `0x0002ebf0` | CNode factory | `new` 0x1c, type 5, visible 1 | +| `0x0002ec40` | CObject::FindProp | PRD walk, `_wcsnicmp` plus full-length check | +| `0x0002ecd0` | CObject::SetProperty | See below | +| `0x0002ee50` | CNode::Render | `m_visible = 0` | +| `0x0002ee60` | CNode::Advance | Runs the behavior script, see below | +| `0x0002eef0` | CNode::GetGroundHeight | Returns 0.0 | +| `0x0002ef00` | CNode::GetBBox | Zeroes the six floats | +| `0x0002ef30` | CNode::GetRadius | Returns 0.0 | +| `0x0002ef40` | CNode::RenderDynamicTexture | Empty | +| `0x0002ef60` | CNodeArray::RemoveNode | Closes up every slot that holds the node, see quirks | +| `0x0002efa0` | CNodeArray::ReleaseAll | Release every non-null slot, then length, capacity and block to zero | +| `0x0002eff0` | CNodeArray::Allocate | Grow-only: new block, copy the old capacity's worth, free the old block | +| `0x0002f050` | CTimeDepNode factory | `new` 0x30, CNode fields, all timing fields zero | +| `0x0002f0d0` | CTimeDepNode::Advance | See below | +| `0x0002f180` | CIntArray::SetSize | Grow-only reallocation then `m_nSize = n`. CNumArray's identical body was folded into this one | +| `0x0002f450` | CObject::~CObject | `CLerper::RemoveObject(this)`, then Release and free every member define and the list | +| `0x0002f4a0` / `0x0002f4b0` | AddRef / Release | `+1`; `-1` and the deleting destructor at zero | +| `0x0002f4d0` | CObject::IsKindOf | Walks `GetNodeClass()` and its `m_baseClass` chain | +| `0x0002f500` | CObject factory | `new` 0x14, type 0, refcount 1 | +| `0x0002f530` | CObject::ToStr | `new CStrObject(L"[object]")`, string at `0x00021b74` | +| `0x0002f590` | CObject::ToNum | `new CNumObject`, value 0 | +| `0x0002f5d0` | CObject::FindFunction | Linear search of the 0x18 byte FND entries until a null function pointer; `wcslen` must equal the length and `wcsncmp` must match; returns the index or -1 | +| `0x0002f640` | CObject::Call | See below | +| `0x000301c0` | CObject::Dot | See below | +| `0x00030310` | CObject::AddMember | Creates the member list on first use, then always pushes a new define at the head | +| `0x00030380` | CObject::SetMember | Creates the list on first use, then find-or-add the define and store the object | +| `0x000303c0` | CObject::GetMember | Null when there is no list, otherwise a lookup by name | +| `0x000303e0` | CObject::FindMemberFunction | Own members first; then the first class up the chain with any member of that name decides (a non-function there means not found) | +| `0x00033970` | CClass::CClass | Registers a `[class]` record with no factory, base or PRD map, clears its own fields, type 6 | +| `0x00033a20` | CClass::~CClass | Frees the URL, Releases the constructor, ReleaseAll on the instance functions, tears down the file and the class record | +| `0x00033ad0` | CClass::CreateNode | `new CInstance(this)`, 0x54 bytes | +| `0x00033b30` | CClass::Load | Saves the current directory into a 2048 byte local, moves it to the file's folder, fetches, parses, and restores it on every exit | +| `0x00033c70` | CClass::LoadAbsURL | The same fetch and parse with no directory change | +| `0x00033d00` | CClass::ParseFile | Sets the parser's file name, line 1 and error flag, parses the class body, reports `Stuff past expected end of file!` if text remains, returns no-error | +| `0x00033d60` | CClass::ParseClassBody | Class compiler over the body, stores the built function as the constructor | +| `0x00033df0` | CInstance::CInstance | CGroup init (bbox size -1.0 from `0x0002bef0`), type 11, AddRef the class, size `m_vars` to the class's variable count with null slots, Construct, then OnLoad | +| `0x00033f00` | CInstance::Construct | See below | +| `0x000342c0` | CInstance::~CInstance | Release the class, ReleaseAll the variables, CGroup teardown | +| `0x00034330` | CInstance::Dot | Class members first, then CObject::Dot | + +### SetProperty (`0x0002ecd0`) + +The byte count arrives in EAX. The field address is `this + prd->pbOffset`. + +1. For a `pt_string` (10) property, if both the current and the new string are non-null and `wcscmp` says they're equal, it returns before anything else, so OnSetProperty never sees a no-op string write. +2. `OnSetProperty` (vtable `+0x08`) can veto the write. +3. By type: + - `pt_string`: free the old buffer, allocate `count * 2 + 2` bytes, copy `count` characters and terminate. + - `pt_intarray` (12), `pt_numarray` (13): `SetSize(bytes / 4)`. `pt_vec2array` (14): `bytes / 8`. `pt_vec3array` (15): `bytes / 12`. `pt_vec4array` (16): `bytes / 16`. Then the raw bytes are copied into the array's buffer. + - Anything else: raw copy of the bytes into the field. For `pt_node` (9) the node already in the field is Released first and the new one AddRef'd after. + +### Call (`0x0002f640`, 2818 bytes) + +Reads the FND entry at `GetFunctionMap() + index * 0x18`: function pointer at `+0x00`, signature as a 16-bit value at `+0x10`. Every argument is first replaced in place by its fully dereferenced value (Deref at vtable `+0x24`, repeated until the object stops changing or becomes null). + +The switch covers signatures 0 to 29, with no case for 6. Case 0 passes the raw `(array, count)` through. Every other case checks the count exactly, converts its arguments, makes the call, wraps the result, and Releases the converted temporaries: + +| Case | Shape | Case | Shape | Case | Shape | +|---|---|---|---|---|---| +| 1 | `void ()` | 11 | `float (float, float)` | 21 | `void (object)` | +| 2 | `int ()` | 12 | `void (str)` | 22 | `void (float, float, float)` | +| 3 | `int (int)` | 13 | `void (str, str)` | 23 | `void (float, float, float, float)` | +| 4 | `str (int)` | 14 | `void (int)` | 24 | `int (int, int)` | +| 5 | `str (str)` | 15 | `object ()` | 25 | `int (str)` | +| 7 | `str (int, int)` | 16 | `object (str)` | 26 | `void (int, int, str)` | +| 8 | `str ()` | 17 | `object (object)` | 27 | `float (int)` | +| 9 | `float ()` | 18 | `void (int, int)` | 28 | `void (int, str)` | +| 10 | `float (float)` | 19 | `void (float)` | 29 | `int (int, str)` | +| | | 20 | `void (float, float)` | | | + +- int arguments are `ToNum` (vtable `+0x1c`) truncated to int; float arguments are the CNumObject's value as is. +- string arguments are `ToStr` (vtable `+0x20`); a null text pointer is replaced by the empty string at `0x00021b98`. +- object arguments are passed as is. +- int and float results come back as a new CNumObject (its constructor at `0x00038fa0` takes the value in xmm0). +- Case 6 has no entry and shares the exit with unknown signatures: return null, no message. +- A wrong argument count calls the running script's error routine with `invalid number of parameters` and returns null. + +### Dot (`0x000301c0`) + +Only an objVariable (4) name is resolved, using its text at `+0x1c` and length at `+0x14`: + +1. FindFunction. A hit returns a new 0x1c byte CFunctionObject (type 8) holding the index and `this`, and AddRefs `this`. +2. GetMember (vtable `+0x34`). A hit is AddRef'd and returned as is. +3. FindProp. A hit returns a new CProperty (`0x00038a20`) for `this` and the PRD. +4. Otherwise null. + +The compiled code checks the type for objVariable a second time before step 3, which can never fail at that point. + +### CNode::Advance (`0x0002ee60`) + +The runner's first field is its wake-up time as a double (`0.0`, from `0x0002bef8`, means awake). + +``` +if (no runner) return; +if (wake != 0) { if (now < wake) return; wake = 0; } +current runner = runner; current this = node; +for (;;) { + do { if (!Step()) ResetFunc(); } while (wake == 0); + if (now < wake) break; + wake = 0; +} +``` + +A behavior script runs every frame until it sleeps into the future. When the function returns, the runner restarts it from the top within the same frame. + +### CTimeDepNode::Advance (`0x0002f0d0`) + +``` +CNode::Advance(); +if (!m_isActive) { + if (start <= (float)now && start != lastStart && (stop == 0.0 || now < stop)) + { lastStart = start; m_isActive = 1; OnIsActiveChanged(); } +} else if (start <= stop && stop != start && stop <= (float)now) { + m_isActive = 0; OnIsActiveChanged(); +} +``` + +The start test and the stop test narrow the double clock to float; the `now < stop` test compares at double precision. A node fires once per distinct start time, and a stop time only counts when it is later than the start. + +### CInstance::Construct (`0x00033f00`, 915 bytes) + +Runs the class constructor's bytecode from `m_constructor + 0x18`. It keeps a 100-entry stack of open nodes, a 100-entry stack of open child arrays (starting at the instance's own children at `+0x1c`), a 2048 byte buffer for string literals, and a pending variable slot (-1 when unset). Opcodes 100 to 108 are handled through a jump table; any other byte ends the loop and the function returns a pointer to it. + +| Op | Operands | Effect | +|---|---|---| +| 100 NewNode | `int cch`, `cch` UTF-16 chars | Push the open node, create the class by name (CreateNewObject `0x00037580`), set its parent to the instance, append it to the open array (growing 16 slots at a time). A pending variable slot gets the node (AddRef'd) and is cleared | +| 101 NewNodeProp | 12 byte PRD, then either `103 int n` or `[102 int n] 100 int cch chars` | With 103: store variable `n` into the open node's property. Otherwise create the node as above, store it into the property of the node underneath, Release the creation reference, and fill a pending slot | +| 102 DefNode | `int n` | The next node created is also kept in variable `n` | +| 103 UseNode | `int n` | AddRef variable `n` and append it to the open array | +| 104 EndNode | none | OnLoad (vtable `+0x38`) the open node and pop | +| 105 InitProp | 12 byte PRD, `int cb`, `cb` bytes | SetProperty on the open node. For strings `cb` is a character count: the text is copied into the local buffer and terminated, and the stream skips `cb * 2` bytes | +| 106 InitArray | `int offset` | Push the open array; the new one is the node array at `node + offset` | +| 107 EndArray | none | Pop the open array | +| 108 Function | `int cch`, chars, `int n` | AddRef the class's instance function `n` and SetMember it on the open node under that name | + +Variable and instance function reads are bounds checked (out of range reads give null); the node stacks are not. + +### Quirks + +- **CNodeArray::RemoveNode moves bytes, not pointers.** The close-up copy uses `rep movsd` with a count of `(length - i - 1) >> 2` followed by `(length - i - 1) & 3` single bytes, so it moves `length - i - 1` bytes when it should move that many 4 byte pointers. Removing anything but one of the last few entries leaves stale pointers in the tail. +- **Unknown Call signatures are silent.** Signature 6 and anything above 29 return null without reporting an error. +- **Signature 29** (`int (int, str)`) exists in the dispatcher. Its one user is `LiveAccounts.VerifyPassword` (FND `0x00188de0`). +- **The CNumArray and CIntArray SetSize bodies are one function** in the image; both property types call `0x0002f180`. +- **CNode::OnLoad and CNodeClass::FindProp are not separate functions.** OnLoad carries its own inlined copy of FindMemberFunction, and the class compiler (`CClassCompiler::ParseProps`, `0x00036440`) calls CObject::FindProp directly on a class record, whose GetPropMap returns its `m_rgprd`. diff --git a/docs/decomp/README.md b/docs/decomp/README.md index 35b81a4..8d72f2e 100644 --- a/docs/decomp/README.md +++ b/docs/decomp/README.md @@ -35,7 +35,7 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x00014b3c | `_classCBillboard` | `CBillboard::class` (scene_groups.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the camera-facing quad node. | | 0x0001a06c | `_classCXAppNode` | `CXAppNode::class` (main.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the application root node type. | | 0x0001a098 | `_classCTimeDepNode` | `CTimeDepNode::class` (animation_nodes.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for time-dependent node base. | -| 0x0001a0f0 | `_classCArrayObject` | `CArrayObject::class` (xap_vm.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the script-visible Array type. | +| 0x0001a0f0 | `_classCArrayObject` | `CArrayObject::class` (array.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the script-visible Array type. | | 0x0001a1b0 | `_classCDateObject` | `CDateObject::class` (date_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the script-visible Date type. | | 0x0001a408 | `_classCFile` | `CFile::class` (file_ops.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the File node. | | 0x0001a94c | `_classCVec3Object` | `CVec3Object::class` (xap_vm.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the script-visible Vec3 type. | @@ -54,14 +54,10 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0001eddc | `_classCNavigationInfo` | `CNavigationInfo::class` (animation_nodes.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for VRML97 NavigationInfo (camera control mode). | | 0x0001eed0 | `_classCCameraPath` | `CCameraPath::class` (camera.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the camera-spline node. | | 0x0001efd8 | `_classCHud` | `CHud::class` (hud_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the heads-up overlay node. | -| 0x0001f004 | `_classCPicture` | `CPicture::class` (asset_loader.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the textured-quad picture node. | +| 0x0001f004 | `_classCPicture` | `CPicture::class` (hud_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the textured-quad picture node. | | 0x0001f300 | `_classCMaxMaterial` | `CMaxMaterial::class` (materials.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the 3ds Max material translator node. | | 0x0001f33c | `_classCLayer` | `CLayer::class` (scene_groups.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the 2D-overlay layer node. | | 0x0001f368 | `_classCKeyboard` | `CKeyboard::class` (keyboard_node.cpp) | [Scene Graph](Node.md) | Static `CNodeClass` registration for the on-screen keyboard node. | -| 0x000223e4 | `CMathClass_floor` | `CMathClass::floor` (math_node.cpp) | [Math](Math.md) | Script `Math.floor(x)`. Wraps the C runtime `floor`. | -| 0x00022408 | `CMathClass_pow` | `CMathClass::pow` (math_node.cpp) | [Math](Math.md) | Script `Math.pow(x, y)`. Wraps `pow`. | -| 0x00022410 | `CMathClass_random` | `CMathClass::random` (math_node.cpp) | [Math](Math.md) | Script `Math.random()`. Returns a `[0, 1)` float from the CRT PRNG. | -| 0x00022434 | `CMathClass_sqrt` | `CMathClass::sqrt` (math_node.cpp) | [Math](Math.md) | Script `Math.sqrt(x)`. Wraps `sqrt`. | | 0x0002c570 | `CXApp::CXApp` | globals init in `main.cpp` | [App Shell](#app-shell) | Constructor for the original `CXApp` singleton. Theseus replaced this with module-scope globals and free functions; the binary class is preserved for reference. | | 0x0002cc10 | `Xbox_Init` | `Xbox_Init` (main.cpp) | [App Shell](#app-shell) | Early-boot init: `XOnlineStartup`, drive-letter symlinks, `XSetFileCacheSize`, `XInitDevices`, region detection, network bring-up. Runs before scene graph creation. | | 0x0002d0e0 | `CXApp::Draw` | main loop in `main.cpp` | [App Shell](#app-shell) | Per-frame render entry. Calls scene graph traversal then `Present`. | @@ -74,8 +70,22 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0002def0 | `XAppCreateFile` | `TheseusCreateFile` in `main.cpp` | [App Shell](#app-shell) | Wrapper around `CreateFileA` with dashboard-specific share/disposition defaults. | | 0x0002ea20 | `CNodeClass::FindByName` | `CNodeClass::FindByName` in `node_system.cpp` | [Scene Graph](Node.md) | Walks the static class registration list (built from `_classCXxxx_CXxxx__2VCNodeClass__A` entries in .data) looking for a UTF-16 class name match. The XAP compiler uses this when binding `DEF` declarations. | | 0x0002ee60 | `CXAppNode::Advance` | tick loop in `main.cpp` | [Scene Graph](Node.md) | Per-frame tick on the application root node. Drives the time-dependent node graph. | -| 0x00030570 | `CArrayElementReference::Deref` | `CArrayElementReference::Deref` (xap_vm.cpp) | [Script VM](VM.md) | Dereferences an `array[index]` LHS. Allocates the slot if missing for assignment paths. | -| 0x00030e70 | `CArrayObject::concat` | `CArrayObject::concat` (xap_vm.cpp) | [Script VM](VM.md) | Array concatenation builtin invoked from script via `Array.concat(...)`. | +| 0x00030440 | `CArrayElementReference_CArrayElementReference` | `CArrayElementReference::CArrayElementReference` (array.cpp) | [Script VM](VM.md) | Holds the array and index an `array[i]` expression names. | +| 0x000304c0 | `CArrayElementReference_dtor` | `CArrayElementReference::~CArrayElementReference` (array.cpp) | [Script VM](VM.md) | Releases the array. | +| 0x00030520 | `CArrayElementReference_Assign` | `CArrayElementReference::Assign` (array.cpp) | [Script VM](VM.md) | Stores into the slot, growing the array when the index is past the end. | +| 0x00030570 | `CArrayElementReference::Deref` | `CArrayElementReference::Deref` (array.cpp) | [Script VM](VM.md) | Reads the slot; out of range is "Array reference out of bounds". | +| 0x00030630 | `CArrayObject_CArrayObject` | `CArrayObject::CArrayObject` (array.cpp) | [Script VM](VM.md) | `new Array(n)` makes n nulls, `new Array(a, b, ...)` holds the arguments. | +| 0x00030770 | `CArrayObject_CArrayObject_Len` | `CArrayObject::CArrayObject(int)` (array.cpp) | [Script VM](VM.md) | Native constructor for an array of n nulls. | +| 0x00030800 | `CArrayObject_dtor` | `CArrayObject::~CArrayObject` (array.cpp) | [Script VM](VM.md) | Releases every element and frees the block. | +| 0x000308e0 | `CArrayObject_Grow` | `CArrayObject::Grow` (array.cpp) | [Script VM](VM.md) | Extends the length, reallocating with 16 spare slots when needed. | +| 0x00030960 | `CArrayObject_ToStr` | `CArrayObject::ToStr` (array.cpp) | [Script VM](VM.md) | `join(",")`. | +| 0x00030970 | `CArrayObject_Dot` | `CArrayObject::Dot` (array.cpp) | [Script VM](VM.md) | `length`, or an element reference for a number. | +| 0x00030a70 | `CArrayObject_concat` | `CArrayObject::concat` (array.cpp) | [Script VM](VM.md) | Script `concat`: new array of both arrays' elements. | +| 0x00030b80 | `CArrayObject_join` | `CArrayObject::join` (array.cpp) | [Script VM](VM.md) | Script `join`: elements as text, nulls as "[null]". | +| 0x00030c40 | `CArrayObject_reverse` | `CArrayObject::reverse` (array.cpp) | [Script VM](VM.md) | Script `reverse`: reversed copy. | +| 0x00030cd0 | `CArrayObject_slice` | `CArrayObject::slice` (array.cpp) | [Script VM](VM.md) | Script `slice(start[, end])`; a negative end counts from one past the last element. | +| 0x00030e00 | `LAB_00030e00` | `CompareAsText` (array.cpp) | [Script VM](VM.md) | Sort comparator: nulls first, then wide string order of ToStr. | +| 0x00030e70 | `CArrayObject_sort` | `CArrayObject::sort` (array.cpp) | [Script VM](VM.md) | Script `sort`: sorted copy. | | 0x00031040 | `CDateObject::Construct` | `CDateObject::Construct` (date_node.cpp) | [Script VM](VM.md) | Date object constructor. Initializes from current system time when called with no args, or parses string/numeric input. | | 0x00031450 | `CDateObject::getDate` | `CDateObject::getDate` (date_node.cpp) | [Script VM](VM.md) | Returns the day-of-month component of the bound date. | | 0x00031510 | `CDateObject::toGMTString` | `CDateObject::toGMTString` (date_node.cpp) | [Script VM](VM.md) | Formats date as RFC 1123 UTC string. | @@ -90,7 +100,25 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x000321f0 | `CNumObject::ToNum` | `CNumObject::ToNum` (xap_vm.cpp) | [Script VM](VM.md) | Number-object self coercion. Returns the bound `float` directly without conversion. | | 0x00032850 | `CStrObject::toLowerCase` | `CStrObject::toLowerCase` (xap_vm.cpp) | [Script VM](VM.md) | Lowercases the bound UTF-16 string in place using `_wcslwr`. | | 0x000328c0 | `CStrObject::toUpperCase` | `CStrObject::toUpperCase` (xap_vm.cpp) | [Script VM](VM.md) | Uppercases the bound UTF-16 string in place using `_wcsupr`. | -| 0x00032af0 | `CMathClass_abs` | `CMathClass::abs` (math_node.cpp) | [Math](Math.md) | Script `Math.abs(x)`. Returns the magnitude of a float. | +| 0x00032950 | `CMathClass_CMathClass` | `CMathClass::CMathClass` (math_node.cpp) | [Math](Math.md) | Registers the "Math" class and its eight float constants. | +| 0x00032a20 | `CMathClass_abs` | `CMathClass::abs` (math_node.cpp) | [Math](Math.md) | Script `Math.abs`. | +| 0x00032a30 | `CMathClass_acos` | `CMathClass::acos` (math_node.cpp) | [Math](Math.md) | Script `Math.acos`. | +| 0x00032a40 | `CMathClass_asin` | `CMathClass::asin` (math_node.cpp) | [Math](Math.md) | Script `Math.asin`. | +| 0x00032a50 | `CMathClass_atan` | `CMathClass::atan` (math_node.cpp) | [Math](Math.md) | Script `Math.atan`. | +| 0x00032a60 | `CMathClass_atan2` | `CMathClass::atan2` (math_node.cpp) | [Math](Math.md) | Script `Math.atan2(a, b)`: arc tangent of b / a. | +| 0x00032a70 | `CMathClass_ceil` | `CMathClass::ceil` (math_node.cpp) | [Math](Math.md) | Script `Math.ceil`. | +| 0x00032a90 | `CMathClass_cos` | `CMathClass::cos` (math_node.cpp) | [Math](Math.md) | Script `Math.cos`. | +| 0x00032aa0 | `CMathClass_exp` | `CMathClass::exp` (math_node.cpp) | [Math](Math.md) | Script `Math.exp`. | +| 0x00032ac0 | `CMathClass_floor` | `CMathClass::floor` (math_node.cpp) | [Math](Math.md) | Script `Math.floor`. | +| 0x00032ae0 | `CMathClass_log` | `CMathClass::log` (math_node.cpp) | [Math](Math.md) | Script `Math.log`. | +| 0x00032af0 | `CMathClass_max` | `CMathClass::max` (math_node.cpp) | [Math](Math.md) | Script `Math.max`. | +| 0x00032b10 | `CMathClass_min` | `CMathClass::min` (math_node.cpp) | [Math](Math.md) | Script `Math.min`. | +| 0x00032b30 | `CMathClass_pow` | `CMathClass::pow` (math_node.cpp) | [Math](Math.md) | Script `Math.pow`. | +| 0x00032b40 | `CMathClass_random` | `CMathClass::random` (math_node.cpp) | [Math](Math.md) | Script `Math.random`: `rand() / 32767`. | +| 0x00032b60 | `CMathClass_round` | `CMathClass::round` (math_node.cpp) | [Math](Math.md) | Script `Math.round`: adds a half and truncates. | +| 0x00032b80 | `CMathClass_sin` | `CMathClass::sin` (math_node.cpp) | [Math](Math.md) | Script `Math.sin`. | +| 0x00032b90 | `CMathClass_sqrt` | `CMathClass::sqrt` (math_node.cpp) | [Math](Math.md) | Script `Math.sqrt`. | +| 0x00032ba0 | `CMathClass_tan` | `CMathClass::tan` (math_node.cpp) | [Math](Math.md) | Script `Math.tan`. | | 0x00033690 | `ExpandCString` | `ExpandCString` (string_util.cpp) | [Util](Util.md) | Resizes a `CString` buffer (the dashboard's growable wide-string container) to hold at least N additional characters. | | 0x00034d80 | `CFunctionCompiler::ParseExpression` | `CFunctionCompiler::ParseExpression` (xap_compile.cpp) | [Script VM](VM.md) | Recursive-descent expression parser. Drives the operator-precedence DOPER table to emit operator opcodes. | | 0x00034ea0 | `CFunctionCompiler::ParseIF` | `CFunctionCompiler::ParseIF` (xap_compile.cpp) | [Script VM](VM.md) | Compiles `if` / `if/else` to `opCond` + jump fixups. | @@ -122,29 +150,57 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0003baf0 | `CRecovery::RecoveryThread` | (reference only) | [App Shell](#app-shell) | Worker thread that performs the original factory-recovery wipe sequence. Theseus does not exercise this on modded hardware. | | 0x0003bf00 | `CSettingsFile::Save` | `CSettingsFile::Save` (settingsfile.cpp) | [Settings](Settings.md) | Serializes the in-memory settings tree to disk. Driven by the settings panel's "Save" action. | | 0x0003ca10 | `CTranslator::TranslateStripColon` | `CTranslator::TranslateStripColon` (locale_node.cpp) | [Locale](Locale.md) | Looks up a localized string by key, then strips a trailing `:` if present. Used for menu labels that need both punctuated and bare forms. | -| 0x0003dba0 | `FindObjectInXIP` | `FindObjectInXIP` (xip_archive.cpp) | [XIP Archives](XIP.md) | Free function: searches an opened XIP archive's directory for a named object (texture, mesh, script). | -| 0x0003ddf0 | `CXipFile::Open` | `CXipFile::Open` (xip_archive.cpp) | [XIP Archives](XIP.md) | Opens a `.xip` archive, reads the header, and validates the magic. Returns a `CXipFile*` handle for subsequent lookups. | -| 0x0003dec0 | `CXipFile::StartLoadThread` | `CXipFile::StartLoadThread` (xip_archive.cpp) | [XIP Archives](XIP.md) | Spawns a background loader thread that streams archive payload while the main thread continues. | -| 0x0003e290 | `CXipFile::Find` | `CXipFile::Find` (xip_archive.cpp) | [XIP Archives](XIP.md) | Linear scan of the archive's TOC for a named entry. | -| 0x0003e320 | `CXipFile::FindObject` | `CXipFile::FindObject` (xip_archive.cpp) | [XIP Archives](XIP.md) | Higher-level wrapper around `Find` that also resolves cross-archive references via the global XIP search list. | +| 0x0003d390 | `FUN_0003d390` | `CXipFile::ReadIndexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Builds an index buffer resource by hand from `XMemAlloc`, retrying through the texture and mesh caches when memory runs out. | +| 0x0003d410 | `FUN_0003d410` | `CXipFile::ReadVertexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Same for a vertex buffer: a 12 byte resource header plus contiguous memory for the vertices. | +| 0x0003d4a0 | `CFileBuffer_FreeBuffer` | `CFileBuffer::SetFile` (xip_archive.cpp) | [XIP Archives](XIP.md) | Takes the file handle and finds the archive's entry in the signed `XIPS` hash section. Theseus keeps the handle only. | +| 0x0003d570 | `CFileBuffer_VerifyXipSignature` | (not built) | [XIP Archives](XIP.md) | Hashes one 64 KB block and compares it with the signed table; a mismatch reboots. Theseus loads unsigned archives. | +| 0x0003d5e0 | `CFileBuffer_Seek` | `CFileBuffer::Seek` (xip_archive.cpp) | [XIP Archives](XIP.md) | Loads the 64 KB block holding a file offset and points the read index into it. | +| 0x0003d710 | `CFileBuffer_Read` | `CFileBuffer::Read` (xip_archive.cpp) | [XIP Archives](XIP.md) | Copies out of the block buffer and refills it a block at a time. A failed `ReadFile` reboots. | +| 0x0003d900 | `LoadXIP` | `LoadXIP` (xip_archive.cpp) | [XIP Archives](XIP.md) | Finds an archive in the 20 slot pool by path or opens the next slot, then loads it on a thread or inline. | +| 0x0003dab0 | `FindInXIPAndDetach` | `FindInXIPAndDetach` (xip_archive.cpp) | [XIP Archives](XIP.md) | Finds a raw entry, newest archive first, and hands its bytes to the caller. | +| 0x0003dba0 | `FindObjectInXIP` | `FindObjectInXIP` (xip_archive.cpp) | [XIP Archives](XIP.md) | Searches every loaded archive, newest first; textures asked of the default archive are looked up as `.xbx`. | +| 0x0003dd20 | `CXipFile_dtor` | `CXipFile::~CXipFile` (xip_archive.cpp) | [XIP Archives](XIP.md) | Frees the tables and names, releases the mesh buffers, frees the block buffer. | +| 0x0003ddf0 | `CXipFile_Open` | `CXipFile::Open` (xip_archive.cpp) | [XIP Archives](XIP.md) | Stores the file name and the lookup key: the path without its extension, with `default` dropped. | +| 0x0003dec0 | `CXipFile_StartLoadThread` | `CXipFile::StartLoadThread` (xip_archive.cpp) | [XIP Archives](XIP.md) | Thread entry: reloads the mesh buffers if a reload is pending, otherwise loads the archive. | +| 0x0003def0 | `CXipFile_Load` | `CXipFile::Load` (xip_archive.cpp) | [XIP Archives](XIP.md) | Opens the file unbuffered, reads the header, tables and name pool, then every entry. | +| 0x0003e030 | `FUN_0003e030` | `ReadTexture` (xip_archive.cpp) | [XIP Archives](XIP.md) | Turns an XPR0 entry into a texture: copies the resource header and moves the pixels to contiguous memory. | +| 0x0003e110 | `CXipFile_CreateObjects` | `CXipFile::CreateObjects` (xip_archive.cpp) | [XIP Archives](XIP.md) | Reads every entry in file order into raw memory, a texture, a mesh reference or a mesh buffer. | +| 0x0003e270 | `LAB_0003e270` | `XipCompareName` (xip_archive.cpp) | [XIP Archives](XIP.md) | `bsearch` compare: `_stricmp` of the wanted name against a directory entry's name. | +| 0x0003e290 | `CXipFile_Find` | `CXipFile::Find` (xip_archive.cpp) | [XIP Archives](XIP.md) | Checks the path starts with this archive's key, then binary searches the sorted directory. | +| 0x0003e320 | `CXipFile_FindObject` | `CXipFile::FindObject` (xip_archive.cpp) | [XIP Archives](XIP.md) | `Find` plus an optional type check; a texture gets an `AddRef` for the caller. | +| 0x0003e390 | `CXipFile_DeleteMeshBuffers` | `CXipFile::DeleteMeshBuffers` (xip_archive.cpp) | [XIP Archives](XIP.md) | Releases all ten vertex and index buffers. | +| 0x0003e440 | `CleanupMeshCache` | `CleanupMeshCache` (xip_archive.cpp) | [XIP Archives](XIP.md) | Under memory pressure, frees the mesh buffers of the archive drawn longest ago. | +| 0x0003e5f0 | `CXipFile_ReadIndexBuffer` | `CXipFile::ReadIndexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Reads an index buffer entry into a new buffer. | +| 0x0003e650 | `CXipFile_ReadVertexBuffer` | `CXipFile::ReadVertexBuffer` (xip_archive.cpp) | [XIP Archives](XIP.md) | Reads the vertex count and FVF, then the vertices into a new buffer. | +| 0x0003e710 | `CXipFile_ReloadMeshBuffers` | `CXipFile::ReloadMeshBuffers` (xip_archive.cpp) | [XIP Archives](XIP.md) | Seeks back to every mesh buffer entry and reads it again after an eviction. | +| 0x0003e800 | `CMeshRef_Render` | `CXipFile::Reload` (xip_archive.cpp) | [XIP Archives](XIP.md) | Mislabeled in the project: flags a reload and starts the loader thread, running it inline if no thread starts. | +| 0x0003e850 | (no function) | `CMeshRef::Render` (xip_archive.cpp) | [XIP Archives](XIP.md) | CMeshRef vtable slot 0: reloads evicted buffers, sets the shader, draws the slice. | +| 0x0003ead0 | (no function) | `CMeshRef::GetFVF` (xip_archive.cpp) | [XIP Archives](XIP.md) | CMeshRef vtable slot 1: the FVF of the buffer the reference draws from. | | 0x0003f420 | `CGameCopier::CreateDirectoryA` | `CGameCopier::CreateDirectory` (file_ops.cpp) | [File Ops](FileOps.md) | Wrapper around the kernel's CreateDirectory with progress-reporting hooks for the copy UI. | | 0x0003f700 | `CGameCopier::CopyFileA` | `CGameCopier::CopyFile` (file_ops.cpp) | [File Ops](FileOps.md) | Buffered file copy with progress callback. Used by save-to-MU and game-copy flows. | | 0x0003f830 | `CGameCopier::DeleteFileA` | `CGameCopier::DeleteFile` (file_ops.cpp) | [File Ops](FileOps.md) | DeleteFile wrapper. | | 0x0003f8a0 | `CGameCopier::DeleteDirectory` | `CGameCopier::DeleteDirectory` (file_ops.cpp) | [File Ops](FileOps.md) | Recursive directory removal with progress callback. | -| 0x000401b0 | `CScreenSaver::Advance` | `CScreenSaver::Advance` (settings_nodes.cpp) | [Settings](Settings.md) | Per-frame tick on the screensaver node. Counts down the idle timer and toggles the saver layer when threshold is reached. | -| 0x000402a0 | `CScreenSaver::reset` | `CScreenSaver::reset` (settings_nodes.cpp) | [Settings](Settings.md) | Resets the idle timer to zero. Called on any input event. | -| 0x00040320 | `CObject::ToNum` | `CObject::ToNum` (xap_vm.cpp) | [Script VM](VM.md) | Default numeric coercion for `CObject` subclasses. Subclasses override; base returns 0. | +| 0x000400f0 | `CScreenSaver_CreateNode` | `CScreenSaver::CScreenSaver` (settings_nodes.cpp) | [Settings](Settings.md) | Factory for "ScreenSaver": enabled, 300 second delay, becomes the global saver. | +| 0x00040190 | `CScreenSaver_dtor` | `CScreenSaver::~CScreenSaver` (settings_nodes.cpp) | [Settings](Settings.md) | Clears the global saver if it's this one. | +| 0x000401b0 | `CScreenSaver::Advance` | `CScreenSaver::Advance` (settings_nodes.cpp) | [Settings](Settings.md) | Fires OnStart, OnDelay2 and OnDelay3 as idle time passes each delay; OnEnd when disabled while active. | +| 0x000402a0 | `CScreenSaver::reset` | `CScreenSaver::reset` (settings_nodes.cpp) | [Settings](Settings.md) | Ends the saver, clears the stages and restarts the idle clock. | +| 0x000402d0 | `CScreenSaver_OnSetProperty` | `CScreenSaver::OnSetProperty` (settings_nodes.cpp) | [Settings](Settings.md) | Enabling resets the idle clock. | +| 0x00040320 | `ResetScreenSaver` | `ResetScreenSaver` (settings_nodes.cpp) | [Settings](Settings.md) | Input hook: resets the saver and the power down timer, returns whether it was showing. | | 0x00040380 | `CObject::OnSetProperty` | `CObject::OnSetProperty` (xap_vm.cpp) | [Script VM](VM.md) | Property-write hook. Called after `opAssign` lands a value on a property; subclasses override to react to property changes. | | 0x00040990 | `CTimeDepNode::~CTimeDepNode` | `CTimeDepNode::~CTimeDepNode` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Destructor for time-dependent nodes (TimeSensor and friends). Unlinks from the time-dep list. | | 0x00040b70 | `CJoystick_EnableGlobalInput` | `CJoystick::EnableGlobalInput` (input.cpp) | [Input](#input) | Toggles dashboard-wide gamepad input. Used by modal scenes (alerts, panic) to suspend or restore controller events. | | 0x0004147c | `CObject::Deref` | `CObject::Deref` (xap_vm.cpp) | [Script VM](VM.md) | Dereferences a script value. For `CVarObject` resolves to the bound target; for primitive objects returns `this`. | | 0x000417f8 | `CTimeDepNode::OnSetProperty` | `CTimeDepNode::OnSetProperty` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Reacts to property writes on time-dep nodes. Re-registers the node with the time-dep list when its `enabled` toggles. | -| 0x00041950 | `CObject::Assign` | `CObject::Assign` (xap_vm.cpp) | [Script VM](VM.md) | Default assignment handler. Called by `opAssign` when LHS is a script-visible property. | -| 0x000419e0 | `CSettings_GetValue` | `CSettings::GetValue` (settings_nodes.cpp) | [Settings](Settings.md) | FND-bound script accessor: returns the value of a settings entry by name. | -| 0x00041f20 | `FUN_00041f20` | `CTitleArray::AddTitle` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Reads `\\TitleMeta.xbx`, extracts `TitleName` for the active language code (or falls back to a translated `"Broken Game"`), then grows the title array by 50 entries if at capacity (`m_titleCount == m_titleCapacity`), and copies the title ID + name into the new slot. Identified by the path-build + INI read + `+= 50` capacity grow + array memcpy pattern. | -| 0x00041c90 | `CTimeSensor::Advance` | `CTimeSensor::Advance` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Per-frame tick. Computes fractional progress through the sensor's cycle interval and routes that into the bound interpolators. | +| 0x00041950 | `CSettings_Dot` | `CSettings::Dot` (settings_nodes.cpp) | [Settings](Settings.md) | Member lookup on a Settings node: its own members first, any other name becomes a setting object bound to that ini entry. | +| 0x000419e0 | `CSettings_SetValue` | `CSettings::SetValue` (settings_nodes.cpp) | [Settings](Settings.md) | FND-bound script call: writes a value into the node's section and saves when it changed. | +| 0x00041a50 | `CSettings_GetValue` | `CSettings::GetValue` (settings_nodes.cpp) | [Settings](Settings.md) | FND-bound script call: returns the value of an entry in the node's section. | +| 0x00041bf0 | `CTimeSensor_CreateNode` | `CTimeSensor::CTimeSensor` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Factory for "TimeSensor": cycleInterval 1.0, enabled. | +| 0x00041c70 | `CTimeSensor_OnSetProperty` | `CTimeSensor::OnSetProperty` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Widens a script write of `time` into the double field. | +| 0x00041c90 | `CTimeSensor::Advance` | `CTimeSensor::Advance` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Per-frame tick: loops or stops at the cycle end, computes fraction_changed and calls the script handler. | +| 0x00041d50 | `CTimeSensor_OnIsActiveChanged` | `CTimeSensor::OnIsActiveChanged` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Turning on stamps the cycle start; turning off calls OnActiveChanged. | | 0x00041d80 | `CObject::ToStr` | `CObject::ToStr` (xap_vm.cpp) | [Script VM](VM.md) | Default string coercion. Returns class name; subclasses override for meaningful output. | | 0x00041d90 | `CObject::_vtable_default` | `CObject::_vtable_default` (xap_vm.cpp) | [Script VM](VM.md) | The default vtable instance shared by base `CObject` instances. | +| 0x00041f20 | `FUN_00041f20` | `CTitleArray::AddTitle` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Reads `\\TitleMeta.xbx`, extracts `TitleName` for the active language code (or falls back to a translated `"Broken Game"`), then grows the title array by 50 entries if at capacity (`m_titleCount == m_titleCapacity`), and copies the title ID + name into the new slot. Identified by the path-build + INI read + `+= 50` capacity grow + array memcpy pattern. | | 0x00042230 | `FUN_00042230` | `CTitleArray::Update` (title_collection.cpp) | [Title Collection](TitleCollection.md) | The boot-time title enumerator. Enters `m_RootLock`, walks `\*.*` via FindFirstFile/FindNextFile, calls AddTitle for each non-`fffe0000` directory, qsorts by name, then ScanSavedGames per title. Identified by the signature match (critical section + FindFirstFile loop + qsort). | | 0x00042740 | `CSavedGameGrid::GetSavedGameCount` | `CSavedGameGrid::GetSavedGameCount` (savegame_grid.cpp) | [Save Games](#save-games) | Returns the total count of saves visible across all attached storage devices, summed from the title array. | | 0x000428d0 | `FUN_000428d0` | | [Title Collection](TitleCollection.md) | Loads / counts DLC entries for a title. Allocates a 0x1000-entry array of stride-0x34 records and iterates `C:\tdata\\$c\` via the FUN_00043730 enumerator. 5960-only DLC content management; no Theseus equivalent. | @@ -156,24 +212,24 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x00042f40 | `FUN_00042f40` | `CTitleArray::RemoveSavedGame` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Second overload taking `(const TCHAR* szTitleID, const TCHAR* szDirName)`. Calls FindTitle, iterates saves by directory name, then memmove-removes. Identified by signature match. | | 0x00042ff0 | `FUN_00042ff0` | | [Title Collection](TitleCollection.md) | Parallel of `RemoveSavedGame` but operates on a stride-0x34 array (DLC) instead of stride-0x30 (saves). 5960 DLC content management; no Theseus equivalent. | | 0x00043150 | `GetSavedGameID_internal` | `CTitleArray::GetSavedGameID` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Returns the directory-name string for the Nth save under the Mth title. The grid-side `CSavedGameGrid::GetSavedGameID2` wrapper trampolines into this on the active device. | +| 0x000432f0 | `CFont::Open` | `CFont::Open` (text.cpp) | [Text](Text.md) | Loads an XTF font file from a XIP and constructs the glyph cache. | | 0x00043390 | `FUN_00043390` | `CTitleArray::GetTitleTotalBlocks` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Computes total block count for a title (cached at title-struct offset +0x20). Calls a path-walking enumerator and aggregates results. Identified by the `wcscmp(... L"fffe0000")` early-out and the +0x20 cache invalidation matching `RemoveSavedGame`. | | 0x00043460 | `FUN_00043460` | | [Title Collection](TitleCollection.md) | Internal path-build helper used by `GetTitleTotalBlocks`: appends a name to a wstring path with a backslash separator, then calls a directory walker. No direct Theseus equivalent. | | 0x000434f0 | `FUN_000434f0` | | [Title Collection](TitleCollection.md) | Save-game directory enumerator. Walks a `\$s\` directory tallying entry sizes. Parallel to FUN_00043730 (DLC enumerator). Inlined / folded into Theseus's `CTitleArray::Update`. | | 0x00043730 | `FUN_00043730` | | [Title Collection](TitleCollection.md) | DLC directory enumerator. Walks `\$c\` to count and size DLC entries. 5960-only DLC content management; folded into Theseus's `CSavedGameGrid::ResolveDLCFolder` flow. | | 0x00043ae0 | `FUN_00043ae0` | | [Title Collection](TitleCollection.md) | Drive-letter to device-unit translator (`'F'`/`'f'`->0, ... `'C'`/`'c'`->8). Used by every CTitleArray method that branches on the active drive. Inlined in Theseus. | | 0x00043c50 | `FUN_00043c50` | `CTitleArray::IsDirty` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Returns 1 if both `m_root[0]` is set AND `m_dirty` is set; 0 otherwise. Two-condition test matches `IsDirty`'s body, not `IsValid`'s single-condition test. | +| 0x00044240 | `CTitleArray::IsPublisherExists` | `CTitleArray::IsPublisherExists` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Tests whether any title in the array shares a given publisher ID. Used by save-game grouping. | | 0x00044290 | `FUN_00044290` | | [Live Accounts](#live-accounts) | Removes ALL Xbox Live accounts from HDD by looping `_XOnlineRemoveUserFromHD` over each loaded user, then invalidating the cache (`this+0x924 = -1`). Not exposed in Theseus; kept here as a binary anchor for the unported bulk-delete flow. | | 0x000442f0 | `FUN_000442f0` | | [Live Accounts](#live-accounts) | Removes a single Xbox Live account from HDD. Per-user inner of `FUN_00044290`. Calls `_XOnlineRemoveUserFromHD` once and invalidates the cache. | -| 0x000432f0 | `CFont::Open` | `CFont::Open` (text.cpp) | [Text](Text.md) | Loads an XTF font file from a XIP and constructs the glyph cache. | -| 0x00044240 | `CTitleArray::IsPublisherExists` | `CTitleArray::IsPublisherExists` (title_collection.cpp) | [Title Collection](TitleCollection.md) | Tests whether any title in the array shares a given publisher ID. Used by save-game grouping. | -| 0x000463f0 | `CMusicCollection_GetSoundtrackCount` | `CMusicCollection::DeleteSoundtrack` (music_collection.cpp, stubbed) | [Music](#music) | Despite the Ghidra label, this trampolines into a delete-shaped helper at 0x00046320 (bounds check, close handle, memmove + count decrement). The Ghidra label appears to be a Version Tracking misalignment. Theseus stubs `DeleteSoundtrack` to no-op. | | 0x00046020 | `FUN_00046020` | `CMusicCollection::FormatSoundtrackSongTime` (music_collection.cpp) | [Music](#music) | Returns a `MM:SS` formatted song-length string. Calls `XGetSoundtrackSongInfo`, divides ms-length by 1000, formats minutes / seconds, wraps in a new `CStrObject`. | | 0x00046120 | `FUN_00046120` | `CMusicCollection::AddSoundtrack` (music_collection.cpp, stubbed) | [Music](#music) | Computes the 20-byte XC digest of the soundtrack name, compares it against a 40-byte two-entry key table for system-soundtrack admission control, then dispatches to `DashAddSoundtrack`. Theseus stubs this to `return -1`. Identified by the XCCalcDigest call shape and the digest-table compare. | | 0x00046320 | `FUN_00046320` | | [Music](#music) | Free-function `::DeleteSoundtrack(int nSoundtrack)` worker. Bounds-checks against `c_nSoundtrackCount`, calls `DashBeginUsingSoundtracks` and `DashDeleteSoundtrack`, then on success closes the dash handle, decrements the count, and memmoves the soundtrack array (stride 0x4c). The wrapper at 0x000463f0 is what FND-binds this. | | 0x00046390 | `FUN_00046390` | | [Music](#music) | Free-function `::DeleteAllSoundtracks()`. Calls `DashBeginUsingSoundtracks`, loops `DashDeleteSoundtrack` over every entry in `c_rgsoundtrack`, closes the dash handle, zeroes `c_nSoundtrackCount`. | +| 0x000463f0 | `CMusicCollection_GetSoundtrackCount` | `CMusicCollection::DeleteSoundtrack` (music_collection.cpp, stubbed) | [Music](#music) | Despite the Ghidra label, this trampolines into a delete-shaped helper at 0x00046320 (bounds check, close handle, memmove + count decrement). The Ghidra label appears to be a Version Tracking misalignment. Theseus stubs `DeleteSoundtrack` to no-op. | | 0x00046400 | `FUN_00046400` | `CMusicCollection::ClearCopyList` (music_collection.cpp, stubbed) | [Music](#music) | Frees the existing `m_rgCopySong` buffer and reallocates `nSongCount * sizeof(CCopySong)` (stride 0x18). Resets `m_nCopySongAlloc` and zeroes `m_nCopySongCount`. Theseus stubs to no-op. | -| 0x00046830 | `FUN_00046830` | | [Music](#music) | `CMusicCollection::ThreadProc` worker. Iterates the copy-song queue (`m_rgCopySong`), reads each song's name via `XGetSoundtrackSongInfo`, hands off to FUN_000468e0 (per-song copy). Theseus does not implement the music copy thread. | | 0x00046660 | `CMusicCollection::GetUpdateString` | `CMusicCollection::GetUpdateString` (music_collection.cpp) | [Music](#music) | Builds the localized "Updating soundtracks..." progress string for the music panel. | +| 0x00046830 | `FUN_00046830` | | [Music](#music) | `CMusicCollection::ThreadProc` worker. Iterates the copy-song queue (`m_rgCopySong`), reads each song's name via `XGetSoundtrackSongInfo`, hands off to FUN_000468e0 (per-song copy). Theseus does not implement the music copy thread. | | 0x00047520 | `~CSavedGameGrid` | `CSavedGameGrid::~CSavedGameGrid` (savegame_grid.cpp) | [Save Games](#save-games) | Destructor. Frees per-title cached data and unhooks the device-change watcher. | | 0x000476f0 | `CSavedGameGrid_GetTitleCount` | `CSavedGameGrid::GetTitleCount` (savegame_grid.cpp) | [Save Games](#save-games) | Returns the count of distinct titles that have at least one save / DLC / Live record on the current device. | | 0x000478d0 | `CSavedGameGrid_GetTitleID` | `CSavedGameGrid::GetTitleID` (savegame_grid.cpp) | [Save Games](#save-games) | Returns the 8-character hex title ID at a given grid index. | @@ -217,19 +273,114 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x0004e0f0 | `CTransform::SetRotation` | `CTransform::SetRotation` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Sets the rotation as axis-angle. Marks the matrix dirty for next CalcMatrix. | | 0x0004e4d0 | `CInline::Init` | `CInline::Init` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Inline-node init: opens the referenced XIP / XAP and inserts its root into the local scene graph. | | 0x0004e740 | `CInline::Advance` | `CInline::Advance` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Per-frame tick. If the load thread completed, swaps the placeholder for the loaded sub-tree. | +| 0x0004f770 | `CViewpoint_CreateNode` | `CViewpoint::CViewpoint` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Factory for "Viewpoint": 45 degree field of view, jump, position (0, 0, 10). | +| 0x0004f830 | `CViewpoint_dtor` | `CViewpoint::~CViewpoint` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Frees the description and unbinds itself. | +| 0x0004f860 | `CViewpoint_OnLoad` | `CViewpoint::OnLoad` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Binds when no viewpoint is bound yet. | +| 0x0004f880 | `BindViewpoint` | `BindViewpoint` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Binds a node if its class chain reaches Viewpoint. | +| 0x0004f8b0 | `CViewpoint_Bind` | `CViewpoint::Bind` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Takes the bound slot and moves the camera to this viewpoint (one second glide unless jump). | +| 0x0004f940 | `CViewpoint_OnSetProperty` | `CViewpoint::OnSetProperty` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | isBound binds; fieldOfView marks the projection dirty. | +| 0x000504d0 | `CNavigationInfo_CreateNode` | `CNavigationInfo::CNavigationInfo` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Factory for "NavigationInfo": avatar (0.25, 1.6, 0.75), headlight, speed 1. | +| 0x00050580 | `CNavigationInfo_dtor` | `CNavigationInfo::~CNavigationInfo` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Frees the type string and unbinds itself. | +| 0x000505b0 | `CNavigationInfo_OnLoad` | `CNavigationInfo::OnLoad` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | First one loaded becomes the bound NavigationInfo. | +| 0x000505e0 | `CNavigationInfo_OnSetProperty` | `CNavigationInfo::OnSetProperty` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | isBound, avatarSize and visibilityLimit mark the projection dirty. | +| 0x00050640 | `CCamera_CreateNode` | `CCamera::CCamera` (camera.cpp) | [Camera](Camera.md) | Factory for "Camera": position (0, 0, 20), identity orientation, idle, clears the noisy flag. | +| 0x00050760 | `CCamera_dtor` | `CCamera::~CCamera` (camera.cpp) | [Camera](Camera.md) | Frees the mode string, then the node destructor. | +| 0x00050780 | `CCamera_UpdateViewMatrix` | `CCamera::UpdateViewMatrix` (camera.cpp) | [Camera](Camera.md) | Builds the view from position and orientation (plus the one degree noisy wobble), flips the third row, loads it into the device. | +| 0x000508e0 | `CCamera_Set` | `CCamera::Set` (camera.cpp) | [Camera](Camera.md) | Starts a timed move to a new pose; zero or negative seconds snaps. | +| 0x000509a0 | `CCamera_Advance` | `CCamera::Advance` (camera.cpp) | [Camera](Camera.md) | Lerps position and slerps orientation through a running move, then rebuilds the view. | +| 0x00050a70 | `CCameraPath_CreateNode` | `CCameraPath::CCameraPath` (camera.cpp) | [Camera](Camera.md) | Factory for "CameraPath": interval 1.0, inactive, no interpolators. | +| 0x00050af0 | `CCameraPath_dtor` | `CCameraPath::~CCameraPath` (camera.cpp) | [Camera](Camera.md) | Releases the position interpolator and drops the orientation one. | +| 0x00050b60 | `CCameraPath_Advance` | `CCameraPath::Advance` (camera.cpp) | [Camera](Camera.md) | Samples the two interpolators at the path's time and snaps the camera there. | +| 0x00050c90 | `CCameraPath_OnSetProperty` | `CCameraPath::OnSetProperty` (camera.cpp) | [Camera](Camera.md) | Writing isActive restamps the start time. | | 0x00053ed0 | `CFont::LoadGlyph` | `CFont::LoadGlyph` (text.cpp) | [Text](Text.md) | Decompresses a single glyph bitmap from the XTF cache table. | -| 0x0005acb0 | `CKeyboard_CursorRight` | `CKeyboard::CursorRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Right-arrow handler on the on-screen keyboard. Wraps to row start when at row end. | -| 0x0005b340 | `CKeyboard_activate` | `CKeyboard::activate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Commits the highlighted key. Handles regular keys, layout switches (shift/sym), and the OK / cancel softkeys. | -| 0x0005b630 | `CKeyboard::Backspace` | `CKeyboard::Backspace` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Handles backspace on the on-screen keyboard. Removes the last character (with surrogate pair awareness). | -| 0x0005b700 | `CKeyboard_CursorLeft` | `CKeyboard::CursorLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Left-arrow handler. Wraps to row end when at row start. | -| 0x0005b740 | `CKeyboard_CursorRight` | `CKeyboard::CursorRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Right-arrow handler (alternate FND-bound entry). | -| 0x0005b780 | `CKeyboard_Shift` | `CKeyboard::Shift` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Toggles the shift / caps layer of the on-screen keyboard. | -| 0x0005c250 | `CLevel_load` | | [Scene Groups](SceneGroups.md) | FND-bound script method on Level: triggers asynchronous load of the Level's referenced sub-scene. | +| 0x00056110 | `GenerateRadialAlphaMask` | `GenerateRadialAlphaMask` (materials.cpp) | [Materials](Materials.md) | Draws the round alpha falloff the four mask textures use. | +| 0x000562d0 | `GetRadialAlphaMask` | `GetRadialAlphaMask` (materials.cpp) | [Materials](Materials.md) | Plain round mask texture, built once. | +| 0x000563a0 | `GetRadialAVAlphaMask` | `GetRadialAVAlphaMask` (materials.cpp) | [Materials](Materials.md) | Round mask squeezed into the top left 0.6875. | +| 0x00056470 | `GetRadialEdgeAlphaMask` | `GetRadialEdgeAlphaMask` (materials.cpp) | [Materials](Materials.md) | Steep edge mask; icons use it. | +| 0x00056540 | `GetRadialEdgeAlphaMainMask` | `GetRadialEdgeAlphaMainMask` (materials.cpp) | [Materials](Materials.md) | Softer edge mask for the main panels. | +| 0x00056610 | `CInnerWallMatInfo_Setup` | `CInnerWallMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Textured falloff drawn from both sides. | +| 0x000567d0 | `CFreeSpaceMatInfo_Setup` | `CFreeSpaceMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Memory gauge: slides the texture by the material's param. | +| 0x00056940 | `CMaskTextureMatInfo_Setup` | `CMaskTextureMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Mesh texture cut by a mask on stage 1, faded on stage 2. | +| 0x00056a50 | `COrangeEggGlowMatInfo_Setup` | (reference only) | [Materials](Materials.md) | Camera space texture projection with an animated offset. | +| 0x00056c70 | `CModulateTextureMatInfo_Setup` | `CModulateTextureMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Mesh texture with a pulsing tinted alpha. | +| 0x00056d90 | `CReflectMatInfo_Setup` | `CReflectMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Environment reflection shader. | +| 0x00056ea0 | `CBackingMatInfo_Setup` | `CBackingMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Colored panel shaped by a mask. | +| 0x00056f80 | `CEggGlowPulseMatInfo_Setup` | `CEggGlowPulseMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Falloff whose front alpha pulses 64..192. | +| 0x00057000 | `CEggGlowFadeMatInfo_Setup` | `CEggGlowFadeMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Falloff glow that fades out over 1.25 seconds after param is set to 1. | +| 0x000570c0 | `CKeyMatInfo_CKeyMatInfo` | `CKeyMatInfo::CKeyMatInfo` (materials.cpp) | [Materials](Materials.md) | Falloff fields plus the bright and text bytes. | +| 0x00057120 | `CKeyMatInfo_Setup` | `CKeyMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Keyboard key colors from one of two palettes. | +| 0x000572d0 | `CIconMatInfo_Setup` | `CIconMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Current or selected title image, fading in, with soft edges. | +| 0x00057650 | `CMatInfo_Setup` | `CMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Base Setup: standard material and cull none flags. | +| 0x000576b0 | `CSolidMatInfo_Setup` | `CSolidMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Flat texture factor color. | +| 0x00057790 | `CFalloffMatInfo_Setup` | `CFalloffMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Side to front color falloff shader. | +| 0x00057940 | `CFalloffTexInfo_Setup` | `CFalloffTexInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Falloff over the mesh's texture. | +| 0x00057af0 | `CAnisoMatInfo_Setup` | `CAnisoMatInfo::Setup` (materials.cpp) | [Materials](Materials.md) | Falloff through the anisotropic shader. | +| 0x00057cb0 | `Material_Init` | `Material_Init` (materials_retail.inl) | [Materials](Materials.md) | Builds and sorts all 127 materials. | +| 0x0005a0f0 | `CMaxMaterial_OnSetProperty` | `CMaxMaterial::OnSetProperty` (materials.cpp) | [Materials](Materials.md) | A new name rebinds; param is widened to a double. | +| 0x0005a130 | `CMaxMaterial_Render` | `CMaxMaterial::Render` (materials.cpp) | [Materials](Materials.md) | Binds the named material and runs its Setup. | +| 0x0005ab80 | `CKeyboard_CKeyboard` | `CKeyboard::CKeyboard` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Constructor. Zeroes every field except the cursor, which starts at -1 (whole string selected). | +| 0x0005ac20 | `CKeyboard_dtor` | `CKeyboard::~CKeyboard` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Releases the text, frame and control nodes, frees `string`, releases the key nodearray. | +| 0x0005acb0 | `CKeyboard_GetAsteriskFormattingState` | `CKeyboard::GetAsteriskFormattingState` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Returns the global *heading* text flag at 0x0017e74c. | +| 0x0005acc0 | `CKeyboard_SetAsteriskFormattingState` | `CKeyboard::SetAsteriskFormattingState` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Sets the global *heading* text flag at 0x0017e74c. | +| 0x0005ace0 | `CKeyboard_selectKey` | `CKeyboard::selectKey` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Moves the highlight to (row, column), falling back to 0 for either one out of range. | +| 0x0005ad40 | `CKeyboard_selectUp` | `CKeyboard::selectUp` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight up one row, wrapping to the bottom and skipping blank cells. | +| 0x0005adb0 | `CKeyboard_selectDown` | `CKeyboard::selectDown` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight down one row, wrapping to the top and skipping blank cells. | +| 0x0005ae60 | `CKeyboard_selectLeft` | `CKeyboard::selectLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight left, wrapping, skipping blanks; on western grids hops across the wide bottom keys. | +| 0x0005af50 | `CKeyboard_selectRight` | `CKeyboard::selectRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Highlight right, the mirror of selectLeft. | +| 0x0005b040 | `CKeyboard_TIDactivate` | `CKeyboard::TIDactivate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Turns the last USB keyboard keystroke into a key press and reports it through TIDKeyUsed / PlaySound / CloseKeyboard. | +| 0x0005b340 | `CKeyboard_activate` | `CKeyboard::activate` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Presses the highlighted key: page and case keys, cursor keys, BACKSPACE, SPACE, DONE (trim, copy to `string`, OnDone) or a character insert. | +| 0x0005b630 | `CKeyboard_Backspace` | `CKeyboard::Backspace` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Removes the character left of the cursor, OnError at the start. | +| 0x0005b6a0 | `CKeyboard_Delete` | `CKeyboard::Delete` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Removes the character under the cursor, OnError at the end. | +| 0x0005b700 | `CKeyboard_CursorLeft` | `CKeyboard::CursorLeft` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Cursor left one, OnError at the start. From a full selection it lands at 0. | +| 0x0005b740 | `CKeyboard_CursorRight` | `CKeyboard::CursorRight` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Cursor right one, OnError at the end. From a full selection it lands at the end. | +| 0x0005b780 | `CKeyboard_Shift` | `CKeyboard::Shift` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Toggles shift on the letter and accent pages, OnError on symbols. | +| 0x0005b7b0 | `CKeyboard_CycleMode` | `CKeyboard::CycleMode` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Steps mode and jmode together through 0, 1, 2. | +| 0x0005b7e0 | `CKeyboard_Insert` | `CKeyboard::Insert` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Script fn. Inserts a string at the cursor, OnError if it would pass 31 characters. | +| 0x0005b890 | `FUN_0005b890` | `IsGridChar` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Whether a typed character appears (as is or uppercased) on any page of the current grid. Used by TIDactivate. | +| 0x0005b970 | `CKeyboard_OnSetProperty` | `CKeyboard::OnSetProperty` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Setting `string` loads the edit buffer (up to 31 characters) with the whole thing selected. | +| 0x0005b9d0 | `CKeyboard_Render` | `CKeyboard::Render` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Draws the frame, the text node with the edit string and cursor, then one node per key with its label. | +| 0x0005bca0 | `CKeyboard_Advance` | `CKeyboard::Advance` (keyboard_node.cpp) | [Keyboard](Keyboard.md) | Advances the node, then the control node. | +| 0x0005bce0 | `CMesh_ScalarDeletingDtor` | `CMesh::~CMesh` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | CMesh vtable (`0x00029d60`) slot 2: destructor, then free. | +| 0x0005bd00 | `CMesh_dtor` | `CMesh::~CMesh` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Releases the vertex and index buffers. | +| 0x0005bdb0 | `CMesh_Create_File` | `CMesh::Create` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Reads the 0x18 byte `.xm` header, maps type 4/5 to list/strip (anything else fails), reads the vertices and indices straight into new buffers. | +| 0x0005bea0 | `FUN_0005bea0` | `CMesh::Render` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | CMesh slot 0. Sets the FVF, swaps edge AA for multisample past 800 faces, gives the first mesh of the frame the camera's own view, draws. | +| 0x0005c050 | `CMeshNode_CreateNode` | `CMeshNode::CMeshNode` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Factory for "Mesh": dirty, owns its mesh, pushed on the front of the mesh node list. | +| 0x0005c0e0 | `CMeshNode_dtor` | `CMeshNode::~CMeshNode` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Frees an owned mesh and the url, unlinks the node. | +| 0x0005c1f0 | `CMeshNode_vfunc23` | `CMeshNode::Init` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Clears the dirty flag. | +| 0x0005c200 | `CMeshNode_Render` | `CMeshNode::Render` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Stamps the render time, loads on first use, draws the mesh and sets its FVF unless the shape renderer already did. | +| 0x0005c250 | `Xap_load` | `CMeshNode::load` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Script fn `load` (FND `0x00188418`). XIP first; else the file beside the scene, then beside the app. | +| 0x0005c320 | `CreateMesh_File` | `LoadMesh` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | New CMesh read from a path. Comes back even when the file didn't open. | +| 0x0005c390 | `MakeSphere` | `MakeSphere` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | D3DX sphere cloned to XYZ, normal, diffuse, then repacked with one DWORD per normal. | | 0x0005d200 | `CLevel::Deactivate` | `CLevel::Deactivate` (scene_groups.cpp) | [Scene Groups](SceneGroups.md) | Marks a Level node inactive. Pauses its time-dep children and skips it during render traversal. | -| 0x0005d300 | `CPositionInterpolator::Interpolate` | `CPositionInterpolator::Interpolate` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Linear interpolation between key positions based on the bound TimeSensor's fraction. | -| 0x0005d5a0 | `COrientationInterpolator::Interpolate` | `COrientationInterpolator::Interpolate` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Spherical-linear interpolation between key axis-angle rotations. | -| 0x00061090 | `CBackgroundLoader::Fetch` | `CBackgroundLoader::Fetch` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Pulls the next pending request off the background loader queue. The loader thread spins on this. | -| 0x00061760 | `CImageTexture::Load` | `CImageTexture::Load` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Loads an XBX (Xbox swizzled) texture from a XIP entry into a D3D texture. | +| 0x0005d300 | `CPositionInterpolator::Interpolate` | `CPositionInterpolator::Interpolate` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Catmull-Rom between interior keys, straight lines on the end spans, clamped at both ends. | +| 0x0005d520 | `COrientationInterpolator_OnLoad` | `COrientationInterpolator::OnLoad` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Converts each axis+angle key into a quaternion once. | +| 0x0005d5a0 | `COrientationInterpolator::Interpolate` | `COrientationInterpolator::Interpolate` (animation_nodes.cpp) | [Animation](AnimationNodes.md) | Slerp between neighboring keys, clamped at both ends. | +| 0x00060390 | `CStarField_CreateNode` | `CStarField::CStarField` (starfield.cpp) | [TMAP System](TmapSystem.md) | Factory for "StarField": 20 stars, speed 1. | +| 0x00060430 | `CStarField_Advance` | `CStarField::Advance` (starfield.cpp) | [TMAP System](TmapSystem.md) | Moves every star toward the viewer and respawns the ones that pass it. | +| 0x00060510 | `CStarField_RenderDynamicTexture` | `CStarField::RenderDynamicTexture` (starfield.cpp) | [TMAP System](TmapSystem.md) | Perspective-projects the stars into the 8-bit surface, brighter when closer. | +| 0x00060640 | `FUN_00060640` | `CStarField::Update` (starfield.cpp) | [TMAP System](TmapSystem.md) | Grows the star array when `stars` goes up, keeping the old stars. | +| 0x000606d0 | `CDotField_CreateNode` | `CDotField::CDotField` (dotfield_node.cpp) | [TMAP System](TmapSystem.md) | Factory for "DotField": spacing 8, still. | +| 0x00060790 | `CDotField_Advance` | `CDotField::Advance` (dotfield_node.cpp) | [TMAP System](TmapSystem.md) | Scrolls the grid; with no speeds set it picks a new heading and spacing every 10 to 15 seconds. | +| 0x00060930 | `CDotField_RenderDynamicTexture` | `CDotField::RenderDynamicTexture` (dotfield_node.cpp) | [TMAP System](TmapSystem.md) | Writes 255 at every grid point. | +| 0x00060990 | `CBackgroundTexture_OnComplete` | `ParseTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | File bytes to a texture: `.xt` raw, `.xbx` XPR0 with header and size checks, anything else through D3DX with the out of memory retry. | +| 0x00060c90 | `FUN_00060c90` | `LoadTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Forces the `.xbx` extension and looks the texture up in the XIPs. | +| 0x00060d40 | `CreateTexture` | `CreateTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | One level managed texture; hands back the size it came out at. | +| 0x00060db0 | `CTexture_CreateNode` | `CTexture::CTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Factory for "Texture": repeats on, A8R8G8B8, no surface. | +| 0x00060e30 | `CTexture_dtor` | `CTexture::~CTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Releases the surface, then the node destructor. | +| 0x00060ec0 | `CTexture_vfunc23` | `CTexture::Create` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Stores the size and makes the surface. | +| 0x00060ef0 | `CImageTexture_CreateNode` | `CImageTexture::CImageTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Factory for "ImageTexture": no url, dirty. | +| 0x00060f70 | `CImageTexture_dtor` | `CImageTexture::~CImageTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Frees the url, then the CTexture destructor. | +| 0x00060f90 | `CImageTexture_GetTextureSurface` | `CImageTexture::GetTextureSurface` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Loads the url the first time after it changes, returns the surface. | +| 0x00060fc0 | `FUN_00060fc0` | `CBackgroundLoader::CBackgroundLoader` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Clears the loader and queues it at the tail of the list at `0x0017e6f0`. | +| 0x00061030 | `FUN_00061030` | `CBackgroundLoader::~CBackgroundLoader` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Unlinks, closes the file, frees the url and the data. | +| 0x00061090 | `CBackgroundLoader_Fetch` | `CBackgroundLoader::Fetch` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Opens the file overlapped (unbuffered on `c:` and `y:`), sector rounds the size, starts one ReadFile for all of it. | +| 0x000611f0 | `FUN_000611f0` | `CBackgroundTexture::~CBackgroundTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Clears the cache slot's loading flag. | +| 0x00061210 | `FUN_00061210` | `CBackgroundTexture::OnComplete` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Parses the loaded file into the cache slot; a 64 or 128 wide request that fails gets the Xbox logo. | +| 0x00061290 | `CleanupTextureCache` | `CleanupTextureCache` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Frees the least recently used texture that isn't locked or loading. First stop of every out of memory retry. | +| 0x000614f0 | `FUN_000614f0` | `UnloadTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Drops every cache slot holding a url and says whether there was one. Called by the MOTD clear. | +| 0x000615a0 | `FindTexture` | `FindTexture` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Cache lookup by url; a miss takes a free or the oldest slot and loads now or in the background. | +| 0x00061760 | `GetTexture` | `CImageTexture::Load` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | Swaps the surface for the cached texture of a url, synchronously, and takes its size. | +| 0x00061820 | `CImageTexture_OnSetProperty` | `CImageTexture::OnSetProperty` (asset_loader.cpp) | [Asset Loader](AssetLoader.md) | A new url marks the texture dirty. | | 0x00061b40 | `CAudioClip::Cleanup` | `CAudioClip::Cleanup` (audio_system.cpp) | [Audio](AudioSystem.md) | Releases an audio clip's DirectSound buffer and waveform data. | | 0x00061ba0 | `CAudioClip::Init` | `CAudioClip::Init` (audio_system.cpp) | [Audio](AudioSystem.md) | Loads a clip from a XIP entry, parses the WAV header, allocates a DSound buffer, and uploads the PCM payload. | | 0x000629a0 | `CAudioBuf::Lock` | `CAudioBuf::Lock` (audio_system.cpp) | [Audio](AudioSystem.md) | DSound `IDirectSoundBuffer::Lock` wrapper. Used by the audio pump to write streamed samples. | @@ -250,8 +401,10 @@ When in doubt about an entry, run the address through Ghidra and compare. The po | 0x00069730 | `CLiveAccounts_GetNumberOfAccounts` | `CLiveAccounts::GetNumberOfAccounts` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns the count of accounts loaded by `LoadFromHD`. | | 0x00069750 | `CLiveAccounts_GetAccountName` | `CLiveAccounts::GetAccountName` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns gamertag for the Nth loaded account. | | 0x000697f0 | `CLiveAccounts_GetAccountName` | `CLiveAccounts::GetAccountName` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Overload of `GetAccountName`. Differs by signature; both write the same gamertag string. | -| 0x000698b0 | `CLiveAccounts_IsPasswordEnabled` | `CLiveAccounts::IsPasswordEnabled` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Tests whether the bound account requires a passcode at logon. | -| 0x000698e0 | `CLiveAccounts_VerifyPassword` | `CLiveAccounts::VerifyPassword` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Compares an entered passcode against the account's stored hash. | +| 0x000698b0 | `CLiveAccounts_IsPasswordEnabled` | `CLiveAccounts::IsPasswordEnabled` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns bit 0 (require passcode) of the account's user options; 0 for an index past the list. | +| 0x000698e0 | `CLiveAccounts_VerifyPassword` | `CLiveAccounts::VerifyPassword` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Encodes the entered text with `XLive_EncodePasscode` and compares the 4 bytes with the account's stored pad code; an index past the list passes. Call signature 29. | +| 0x00069930 | `XLive_EncodePasscode` | `EncodePasscode` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Maps up to 4 characters to pad codes: `u d l r x y L R` to 1 2 3 4 5 6 9 10, others keep their low byte, missing ones are 0. | +| 0x000699e0 | `CLiveAccounts_Logon` | `CLiveAccounts::Logon` (live_accounts.cpp) | [Live Accounts](#live-accounts) | A non-empty passcode that doesn't match the stored pad code fires `OnInvalidPassword`; otherwise starts `XOnlineLogon` and sets `currentAccount`. | | 0x00069ae0 | `CLiveAccounts_Logoff` | `CLiveAccounts::Logoff` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Logs out the currently signed-in Live user and clears in-memory credentials. | | 0x0006a000 | `CLiveAccounts_IsBackFromEntryPoint` | `CLiveAccounts::IsBackFromEntryPoint` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns whether the dashboard re-entered the Live flow from a title (post-game return) versus a fresh boot. | | 0x0006a030 | `CLiveAccounts_GetLastLogonUser` | `CLiveAccounts::GetLastLogonUser` (live_accounts.cpp) | [Live Accounts](#live-accounts) | Returns the index of the account that last logged on. Used to default the user selector. | @@ -271,19 +424,23 @@ The seed and first expansion cover the application shell, script VM, scene graph |-----------|-----|----------| | Script VM | [VM.md](VM.md) | 35 functions | | Scene Graph | [Node.md](Node.md) | 2 functions + 32 class-table registrations | -| XIP Archives | [XIP.md](XIP.md) | 5 functions | -| Animation Nodes | [AnimationNodes.md](AnimationNodes.md) | 4 functions | +| XIP Archives | [XIP.md](XIP.md) | 26 functions | +| Animation Nodes | [AnimationNodes.md](AnimationNodes.md) | 17 functions | | Audio System | [AudioSystem.md](AudioSystem.md) | 12 functions | -| Camera | [Camera.md](Camera.md) | not yet started | -| Date | [Date.md](Date.md) | 7 functions | -| Math | [Math.md](Math.md) | 5 functions | +| Camera | [Camera.md](Camera.md) | 9 functions | +| Date | [Date.md](Date.md) | 30 functions | +| Math | [Math.md](Math.md) | 19 functions | +| Materials | [Materials.md](Materials.md) | 25 functions | | File Operations | [FileOps.md](FileOps.md) | 7 functions | +| File Utilities | [FileUtil.md](FileUtil.md) | 10 functions: paths, URL resolving, block counts | | Disc | (no doc yet, narrative TODO) | 1 function | -| Keyboard | [Keyboard.md](Keyboard.md) | 6 functions | -| Locale | (no doc yet, narrative TODO) | 1 function | +| HUD & Picture | [HudNode.md](HudNode.md) | 9 functions | +| Keyboard | [Keyboard.md](Keyboard.md) | 22 functions | +| Locale | [Locale.md](Locale.md) | 20 functions: translation table, Translator node, date/time/number formats, time zones | | NtIoSvc | [NtIoSvc.md](NtIoSvc.md) | 1 function | | Scene Groups | [SceneGroups.md](SceneGroups.md) | 6 functions | -| Settings | [Settings.md](Settings.md) | 14 functions | +| Settings | [Settings.md](Settings.md) | 28 functions: settings file, Settings and ScreenSaver nodes | +| Screen & Recovery | [Screen.md](Screen.md) | 9 functions | | Shape Render | [ShapeRender.md](ShapeRender.md) | not yet started | | Text | [Text.md](Text.md) | 2 functions | | Title Collection | [TitleCollection.md](TitleCollection.md) | 9 verified Theseus mappings (AddTitle, Update, FindTitle, RemoveTitle, RemoveSavedGame x2, GetTitleTotalBlocks, IsDirty, IsPublisherExists) + 6 binary-only anchors (DLC enumerator, save dir walker, drive-letter translator, etc.); more labeling pending | @@ -291,12 +448,12 @@ The seed and first expansion cover the application shell, script VM, scene graph | Util | [Util.md](Util.md) | 1 function | | Save Games | (no doc yet, narrative TODO) | 29 functions | | Live Accounts | (no doc yet, narrative TODO) | 13 verified Theseus mappings + 2 binary-only anchors (bulk delete, single-user delete) | -| Input | (no doc yet, narrative TODO) | 1 function | +| Input | [Input.md](Input.md) | 9 functions | | Memory Monitor | (no doc yet, narrative TODO) | 12 functions | | Music Collection | (no doc yet, narrative TODO) | 5 verified Theseus mappings (FormatSoundtrackSongTime, GetUpdateString + 3 stubs: AddSoundtrack, ClearCopyList, DeleteSoundtrack) + 3 binary-only anchors (global ::DeleteSoundtrack, ::DeleteAllSoundtracks, ThreadProc); more labeling pending | | Render | (no doc yet, narrative TODO) | 1 function | | App Shell | (no doc yet, narrative TODO) | 9 functions | -| Asset Loader | [AssetLoader.md](AssetLoader.md) (narrative TODO) | 2 functions | +| Asset Loader | [AssetLoader.md](AssetLoader.md) | 30 functions | | DVD Playback | (reference-only, no doc) | 1 function | | Original WMA Pump | (reference-only, folded into AudioSystem.md) | 1 function | | Original CDDA Streamer | (reference-only, folded into AudioSystem.md) | 1 function | diff --git a/docs/decomp/SceneGroups.md b/docs/decomp/SceneGroups.md index d134f34..dc293f8 100644 --- a/docs/decomp/SceneGroups.md +++ b/docs/decomp/SceneGroups.md @@ -80,6 +80,8 @@ Level transition state found in .data section: ### CBackground - `skyColor` (pt_vec3), `backdrop` (pt_node), `isBound` (pt_boolean) +- `fade` (pt_number), `alpha` (pt_number) +- Functions (FND `0x00187f10`): SetAlpha, DisappearAfter ### CLevel - `control` (pt_node), `tunnel` (pt_node), `path` (pt_node), `shell` (pt_node) @@ -95,4 +97,330 @@ Level transition state found in .data section: the retail binary does not spawn a thread for this. - CBackground has no Render() override in the retail binary. Only RenderBackdrop() is used. -- SetLight() methods are present in all retail builds. +- The node vtables have no light slot. Slot 19 (`+0x4c`) of both the + Group and Switch vtables is the shared stub at `0x00041da0`; the slots + around it are GetRadius (18), RenderDynamicTexture (20), GetGroundHeight + (21) and GetTextureSurface (22). + +## Group.cpp, Background, Layer and Level in the 5960 retail XBE + +| Range | Contents | +|---|---| +| `0x0004d3b0` to `0x0004f3d0` | Group, Transform, Inline, Spinner, Waver, Layout, Switch, Billboard | +| `0x0004f3d0` | Billboard's matrix helper (only called from its Render) | +| `0x0004fc40` to `0x000504c0` | Background | +| `0x0005a290` to `0x0005ab30` | Layer | +| `0x0005cbd0` to `0x0005d230` | Level | + +Every world matrix change in this code is the same three steps: a call on the matrix stack at +`0x0017e7fc` (Push `+0x10`, Pop `+0x0c`, LoadIdentity `+0x14`, MultMatrixLocal `+0x20`, +RotateAxisLocal `+0x28`, TranslateLocal `+0x40`), a 16 dword copy of the stack top (`+0x44`) +into the device's world matrix with dirty bit `0x200`, then `0x0005ca00`, which recomputes and +uploads the vertex shader constants. `0x0002c490` is that upload on its own and `0x0002c4d0` is +Pop plus upload. The effect alpha that nodes multiply into is the float at `0x00175ca0`, the +frame clock is the double at `0x0017e808`. + +### Object layouts + +``` +CGroup 0x44 +0x1c children (count, capacity, array) +0x28 bboxCenter + +0x34 bboxSize +0x40 bboxSpecified +0x41 bboxDirty +CTransform 0x1a8 +0x44 rotation (axis, angle) +0x54 center +0x60 scaleOrientation + +0x70 scale +0x7c rotation quaternion +0x8c translation + +0xb4/+0xc0/+0xd0 scale/rotation/translation start + +0xf8/+0x104/+0x114 scale/rotation/translation end + +0x120 alpha +0x124 alpha start +0x128 alpha end + +0x12c moving +0x130 dirty (dword) +0x134 matrix +0x174 fade + +0x178, +0x17c two floats zeroed and never read + +0x180 scale clock +0x188 rotation clock +0x190 translation clock + +0x198 alpha clock +0x1a0 disappear time (all doubles) +CInline 0x2e0 +0x44 url +0x48 preload +0x49 class loaded +0x4a fadeInDelayLoad + +0x4b dirty +0x4c loading +0x4d first load +0x50 XIP + +0x58 time of load (double) +0x60 CClass +0xd0 thread handle + +0xd4 absolute url (260 wide chars) +CSpinner 0x58 +0x44 rpm +0x48 axis +0x54 angle +CWaver 0x5c +0x58 field +CLayout 0x54 +0x44 direction +0x50 spacing +CSwitch 0x2c +0x1c whichChoice +0x20 choice (count, capacity, array) +CBillboard 0x90 +0x44 axisOfRotation +0x50 matrix +CLayer 0x5c +0x44 viewpoint +0x48 navigationInfo +0x4c fade +0x50 transparency + +0x54 vertex buffer +0x58 alpha (not set by the constructor) +CBackground 0x60 +0x1c backdrop +0x20 skyColor +0x2c isBound +0x30 fade + +0x38 alpha clock (double) +0x40 alpha start +0x44 alpha end + +0x48 alpha +0x50 disappear time (double) +0x58 vertex buffer +CLevel 0x68 +0x44 control +0x48 tunnel +0x4c path +0x50 shell +0x54 archive + +0x58 unloadable +0x59 fade +0x5c a float never touched + +0x60 XIP +0x64 arrive +``` + +Defaults: Group bbox size `-1.0` on all three axes with specified and dirty set; Transform +rotation and scaleOrientation `(0,0,1,0)`, scale `(1,1,1)`, alpha 1, dirty 1; Inline +fadeInDelayLoad 1, dirty 1; Spinner rpm 1, axis `(0,1,0)`; Waver field `pi/4`; Layout +direction `(1,0,0)`, spacing 1; Switch whichChoice -1; Billboard axis `(0,1,0)`; Background +alpha 1; Level unloadable 1, fade 1, visible 0. Level's constructor also zeroes the shared +transition clock. Background's constructor makes the new node the bound background (global +`0x0017e824`) when there isn't one yet. + +Level globals: current level `0x0017c8f0`, level being left `0x0017c8ec`, current tunnel +`0x0017c8e8`, transition end time `0x0017c8e0` (double), transition flag `0x0017c8d8`. +The current Level writes the motion blur strength at `0x00179f54`. + +### Function map + +| Address | Function | What it does | +|---|---|---| +| `0x0004d3d0` | Group factory | `new` 0x44, constructor inlined | +| `0x0004d470` | CGroup::~CGroup | ReleaseAll on the children, then CNode teardown | +| `0x0004d4d0` | CGroup::GetBBox | See below | +| `0x0004d770` | CGroup::GetRadius | Largest child radius, starting from 0 | +| `0x0004d7e0` | CGroup::Render | Render each child whose visible byte is set | +| `0x0004d820` | CGroup::Advance | CNode::Advance, then every child | +| `0x0004d860` | CGroup::RenderDynamicTexture | Every non-null child | +| `0x0004d8a0` | CGroup::GetTextureSurface | First non-null child texture | +| `0x0004d910` | CTransform::CTransform | Defaults above | +| `0x0004da40` | CTransform::OnLoad | CNode::OnLoad, then the quaternion from the axis and angle | +| `0x0004da60` | CTransform::OnSetProperty | Rotation rebuilds the quaternion; alpha goes through SetAlpha and returns 0 | +| `0x0004dae0` | CTransform::Advance | Alpha fade step, disappear timer | +| `0x0004dbb0` | CTransform::CalcMatrix | See below | +| `0x0004df30`..`0x0004e250` | Transform script functions | See below | +| `0x0004e270` | CTransform::Render | See below | +| `0x0004e3b0` | CInline::CInline | Defaults above, CClass constructed in place | +| `0x0004e470` | CInline::~CInline | Free the url, CClass teardown, group teardown | +| `0x0004e4d0` | CInline::Init | See below | +| `0x0004e560` | CInline::FinishLoad | See below | +| `0x0004e5d0` | CInline::Render | See below | +| `0x0004e740` | CInline::Advance | See below | +| `0x0004e8c0` | Spinner and Waver Render | Push, rotate about axis by angle, upload, children, Pop | +| `0x0004e980` | CSpinner::Advance | See below | +| `0x0004eaf0` | CWaver::Advance | See below | +| `0x0004ebd0` | CLayout::Render | See below | +| `0x0004ef00` | CLayout::GetBBox | See below | +| `0x0004f000` | CLayout::GetRadius | Sum of child radii, plus `(count - 1) * spacing * 0.5` when count > 1 | +| `0x0004f100` | CSwitch::~CSwitch | ReleaseAll on the choices, CNode teardown | +| `0x0004f160` | CSwitch::Render | Render the picked choice if in range and visible | +| `0x0004f190` | CSwitch::GetBBox | Picked choice's box, else zeros | +| `0x0004f1e0` | CSwitch::GetRadius | Picked choice's radius, else 0 | +| `0x0004f210` | CSwitch::Advance | CNode::Advance, then every choice | +| `0x0004f330` | CBillboard::Render | Matrix helper, Push, MultMatrixLocal, upload, children, Pop | +| `0x0004f3d0` | Billboard matrix | See below | +| `0x0004fc90` | CBackground::CBackground | Defaults above | +| `0x0004fd30` | CBackground::~CBackground | Release the vertex buffer and backdrop, unbind if bound | +| `0x0004fde0`, `0x0004fe20` | Background SetAlpha, DisappearAfter | Same shape as Transform's, on the Background fields | +| `0x0004fe30` | CBackground::Advance | See below | +| `0x00050110` | CBackground::RenderBackdrop | See below | +| `0x00050450` | CBackground::OnSetProperty | isBound true binds; alpha goes through SetAlpha and returns 0 | +| `0x0005a350` | CLayer::~CLayer | Release viewpoint, navigationInfo, vertex buffer | +| `0x0005a400` | CLayer::OnSetProperty | See below | +| `0x0005a4a0` | CLayer::Render | See below | +| `0x0005aaf0` | CLayer::Advance | Group advance, then navigationInfo, then viewpoint | +| `0x0005cc20` | CLevel::CLevel | Defaults above | +| `0x0005ccd0` | CLevel::~CLevel | Clear itself out of the level globals, release tunnel, control, path, shell, free archive | +| `0x0005cd80` | CLevel::Render | See below | +| `0x0005ce90` | CLevel::Activate | See below | +| `0x0005cf90` | CLevel::GoTo | See below | +| `0x0005d040` | CLevel::GoBackTo | See below | +| `0x0005d120` | CLevel::Advance | See below | +| `0x0005d200` | CLevel::Deactivate | Hide, `OnDeactivate`, drop the XIP's mesh buffers when unloadable and not locked | + +### CGroup::GetBBox (`0x0004d4d0`) + +A size of `-1` on every axis clears the specified flag. When dirty and not specified, an empty +group gets a zero box. Otherwise it takes the first child's box as `lo = center - size`, +`hi = center + size`, and for each later child keeps the smaller value on both corners: + +```c +if (b.c - b.s < lo) lo = b.c - b.s; +if (b.c + b.s < hi) hi = b.c + b.s; // smaller, not larger +center = (hi + lo) * 0.5; size = hi - lo; +``` + +So the far corner shrinks rather than grows, and the result is not a union. Dirty is never +cleared, so this reruns on every call. + +### CTransform + +**CalcMatrix (`0x0004dbb0`).** Identity, then for each of scale, translation and rotation +whose clock is above 0: set moving, `t = (now - clock) / fade`, and at `t >= 1` stop the clock +and clamp. Scale and translation lerp component by component; rotation goes through +`D3DXQuaternionSlerp`. The matrix is then built as scale (skipped when `(1,1,1)`), times the +quaternion's rotation (skipped when its w is 0), times translation (skipped when zero). Dirty +ends up as `scaleClock + rotationClock + translationClock > 0`. Center and scaleOrientation +are stored but never used here. + +**Script functions.** SetScale (`0x0004df30`) and SetTranslation (`0x0004e010`): with +`fade > 0`, start the clock at now, copy the current value to start, the arguments to end, set +moving; otherwise store directly. Both set dirty. SetRotation (`0x0004e0f0`) builds the +quaternion first, then either fades from the current quaternion (leaving the axis/angle +property alone) or stores both. SetCenter and SetScaleOrientation store and set dirty. SetAlpha +(`0x0004e200`) fades or stores alpha without touching dirty. DisappearAfter (`0x0004e250`) +stores `t + now` added in single precision, as a double. + +**Advance (`0x0004dae0`).** Group advance, then the alpha fade (same clock math), then if the +disappear time is set and reached, clear it and set alpha to 0. + +**Render (`0x0004e270`).** Clear moving, CalcMatrix when dirty, Push, MultMatrixLocal, upload. +Only with alpha above 0: multiply the effect alpha by it, render the children, restore. Pop +and upload. + +### CInline + +**Init (`0x0004e4d0`).** Clear dirty, ReleaseAll on the children, stop if there's no url. +Make the url absolute and replace everything from its last `/` with `.xip`, so `Foo/default.xap` +names `Foo.xip`. If there was a slash: set loading and first load and call LoadXIP; on success +return and let Advance finish. Otherwise clear loading and FinishLoad right away. + +**FinishLoad (`0x0004e560`).** `LoadAbsURL` on the node's CClass with the absolute url at +`+0xd4`. On success: CreateNode, call its `initialize`, append it to the children (growing 16 +slots at a time), set its parent, call the Inline's own `onLoad`. Then clear first load and +set class loaded. The absolute url is only written by Advance, after the XIP arrives. + +**Render (`0x0004e5d0`).** Nothing without a url or while loading. When the XIP is loaded, not +reloading, has vertex buffers and its first buffer is gone, it sets loading, starts the XIP +reload and returns. Otherwise Init when dirty, and once loaded: `a = (now - timeOfLoad) * 0.5`, +capped at 1 with fadeInDelayLoad, 1 without. The effect alpha is multiplied by `a`, the +current directory is saved and set from the url, the children render, and both are restored. + +**Advance (`0x0004e740`).** Init when dirty and preload. While the XIP is not loaded or is +reloading, set loading and return without advancing the children. On the first frame it is +ready: clear loading; on a first load make the absolute url and, with no thread handle, +FinishLoad; record the time of load. A thread handle that has finished is closed (nothing in +the image creates one). Then Group advance. + +### Spinner and Waver + +**CSpinner::Advance (`0x0004e980`).** Group advance, `angle += rpm * (1/60) * dt * 2pi`, then +wrap into `[-2pi, 2pi]` with a subtract loop above and an add loop below. Constants: `1/60` at +`0x0002c1c4`, `2pi` at `0x0002bf70`, `-2pi` at `0x0002c1c0`. + +**CWaver::Advance (`0x0004eaf0`).** Group advance (not Spinner's), then in x87: + +``` +fld [rpm] +fmul qword [now] +fmul qword [0x0002c1b8] ; pi / 60 +fsin +fmul [field] +fmul [0.5] +fstp [angle] +``` + +### CLayout + +**Render (`0x0004ebd0`).** Nothing for no children. A zero direction lays the children on a +ring of radius `(count / 4) * spacing` (integer divide). Each child gets a Push first; a visible +one is translated by `(cos(-a), 0, sin(-a)) * radius` with `a = i * 2pi / count`, rotated +`a + pi/2` about `(0, 0.1, 0)`, uploaded, rendered, popped. An invisible child's Push is never +popped. Otherwise: one Push, translate by `direction * (count + spacing) * -0.5`, then per +visible child translate `direction * 0.5`, upload, render, and unless it's the last child +translate `direction * (spacing + 0.5)`. Pop at the end. + +**GetBBox (`0x0004ef00`).** Zeros for no children. Otherwise the first child's box, then for +each later child: `size += child.size`, then each axis takes the child's size if that's +smaller. The center is the first child's. + +### Billboard matrix (`0x0004f3d0`) + +Copies the device world matrix, inverts it and brings the camera position (`0x00187fd4`) into +local space with `D3DXVec3TransformNormal`, normalized (the eye). Axis length at most `0.001` +(`0x0002c050`): up is `(inv._14, inv._22, inv._23)` normalized, `x = normalize(up x eye)`, and +the matrix rows are x, up, eye, `(0,0,0,1)`. Otherwise the axis is normalized, +`x = normalize(axis x eye)`, `z = x x axis`, and the matrix is a rotation about the axis by +`acos(z.z)`, negated when `x.z > 0`. + +### CBackground + +**Advance (`0x0004fe30`).** CNode::Advance, the alpha fade and disappear timer (same clock +math as Transform, on the Background fields), the backdrop's Advance, and on first use a 4 +vertex, 32 byte stride buffer at `z = -10000` (`0x0002c1a0`): + +| Vertex | Position | Normal | UV | +|---|---|---|---| +| 0 | `( w/2, -h/2)` | `( w/2, -h/2, 1)` | `(1, 1)` | +| 1 | `(-w/2, -h/2)` | `( w/2, h/2, 1)` | `(0, 1)` | +| 2 | `( w/2, h/2)` | `( w/2, h/2, 1)` | `(1, 0)` | +| 3 | `(-w/2, h/2)` | `(-w/2, h/2, 1)` | `(0, 0)` | + +`w` and `h` are the view size at `0x0017e7f0` / `0x0017e7f4`. + +**RenderBackdrop (`0x00050110`).** Gets the backdrop's texture (vtable `+0x58`) and does +nothing without one. Binds it to stage 0 and clears stage 1, saves projection, world and view, +sets `D3DXMatrixOrthoLH(w, h, -10000, 10000)` with identity world and view. Saves four render +states, clears two of them, sets culling to CCW (`0x901`) and turns blending on; stage 0 color +is the texture and alpha is the texture factor, `(int)(alpha * 255) << 24`. Draws the strip +and restores all of it. + +### CLayer + +**OnSetProperty (`0x0005a400`).** A transparency write removes any lerper on the node, then +either sets alpha to `1 - value` (fade at most 0) or starts a 0x28 byte lerper from the +current alpha to `1 - value` over fade seconds. Every write is accepted. + +**Render (`0x0005a4a0`).** Nothing without a viewpoint or with alpha 0. On first use it +builds a 4 vertex, 16 byte stride buffer of screen corners `( w/2,-h/2)`, `(-w/2,-h/2)`, +`( w/2, h/2)`, `(-w/2, h/2)` at z 0, color `0xff000000`. Then, under the ortho projection with +identity world and view: stage 0 alpha = diffuse times texture factor `(int)(alpha * 255) << 24`, +color = diffuse, texture transform off, texcoord index 0, blending on, culling off, stage 1 +color and alpha off; draws the strip as a triangle strip and restores the three matrices. + +It then walks the navigationInfo's class chain looking for NavigationInfo (`0x00187f58`) but +doesn't act on the answer. Projection: near `0.1`, far `1000` (`0x0002bfc0`, `0x0002c060`); +with a navigationInfo, near is `avatarSize.x * 0.5` and far is `visibilityLimit` when that is +nonzero. Field of view from the viewpoint (`+0x20`), aspect `1.5` (`0x0002c184`) or `1.875` +(`0x0002c1b0`) when the widescreen flag at `0x0017e7ec` is set, written into the shared +projection at `0x0017f4e8`. View: position `(x, y, -z)` (`+0x38`), quaternion about the +orientation axis (`+0x28`) by minus its angle (`+0x34`), `D3DXMatrixAffineTransformation` +with scale 1, inverted, and the third row (`_31` to `_34`) negated, into the shared view at +`0x0017f468`. Push, LoadIdentity, upload, render the children, Pop, then put the saved +projection and view back both in the shared matrices and on the device. + +### CLevel + +**Render (`0x0005cd80`).** With fade: `t = (endTime - now) * 1.3333334`, clamped to `[0, 1]` +(`0x0002c104` is `1 / 0.75`). The current level uses `1 - t`, the level being left uses `t`, +and either one returns early at 0. Any other level renders at full strength. The effect alpha +is multiplied, the shell renders (no visible test), the current level sets the motion blur to +`min(a * 1.3, 1)`, the children render, the effect alpha is restored. + +**Activate (`0x0005ce90`, not script visible).** Set the transition flag. Load the archive XIP +if there is one and it isn't loaded yet. If a transition is still running (a level being +left, or a current level with the end time still set), cut it short: Deactivate the level +being left (inlined) and clear it; hide the current level and drop its XIP's mesh buffers +when it has a XIP, is unloadable and the XIP isn't locked; clear the current level; hide and +clear the tunnel. Then unbind the controller (BindJoystick with no node, inlined), set the +end time to `now + 1.0`, make the current level the one being left, show this level and make +it current. + +**GoTo (`0x0005cf90`).** Nothing if already current. Activate, make this level's tunnel the +current tunnel and show it. A path whose class chain reaches Viewpoint (`0x00187e88`) gets +bound; one that reaches CameraPath (`0x00188040`) is started forward (active `+0x20` = 1, +start `+0x30` = now, backward `+0x21` = 0). Then `OnActivate`. + +**GoBackTo (`0x0005d040`).** Nothing if already current. Activate; the tunnel is the one of +the level being left (none if there isn't one) and is shown. If that level's path is a +CameraPath it is started backward; otherwise a Viewpoint path on this level is bound. Then +`OnActivate`. + +**Advance (`0x0005d120`).** Group advance, then tunnel, path, control and shell. When this is +the current level and the end time is set and reached: clear it, hide the tunnel, Deactivate +and clear the level being left, and set arrive. While arrive is set and the XIP is absent or +loaded (`+0x28`): clear arrive, bind this level's control as the controller, call +`OnArrival`, clear the transition flag. + +### Quirks + +- Group's far corner and Layout's size both shrink toward the smallest child instead of + covering all of them. +- Layout's ring pushes for every child but pops only for visible ones. +- Layer never initializes its alpha; the first transparency write sets it. +- Setting a Background's isBound to false only clears the flag; it stays the bound + background. +- Deactivate reads the XIP's locked flag without checking the XIP exists, so an unloadable + level with no archive faults when it is left. +- An Inline whose XIP fails to load runs FinishLoad with the absolute url never written. +- A Background, being a plain CNode, clears its own visible flag the first time a group + renders it. +- Background and Transform fades keep running if fade is set to 0 mid fade. The next + Advance divides by 0, clamps to the end value and overwrites any alpha stored directly in + the meantime. +- Background's PRD table lists `fade` at `+0x30` and `alpha` at `+0x48` after skyColor, + backdrop and isBound; an `alpha` write goes through the SetAlpha path, so it fades too. diff --git a/docs/decomp/Screen.md b/docs/decomp/Screen.md new file mode 100644 index 0000000..5255291 --- /dev/null +++ b/docs/decomp/Screen.md @@ -0,0 +1,30 @@ +# Screen and Recovery + +Two small nodes from the 5960 retail binary. + +## Screen + +A 0x44 byte node with no script functions; scenes only set its properties (PRD order: width, +height, fullScreen, showFramerate, title, icon, border, letterbox, wideScreen, brightness). + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0004f9c0 | CScreen::CScreen | 640 x 480, flags off, no strings, brightness 1; the first Screen becomes the app's screen (`0x0017e818`) | +| 0x0004fa50 | CScreen::~CScreen | Frees title, icon and border; clears the app's screen if it is this one | +| 0x0004fa90 | CScreen::OnSetProperty | width or height mark the size dirty, title the title; a brightness other than 1 drops any running lerp and starts a 2 second one instead of storing the value | +| 0x0004fb20 | CScreen::Advance | Clears both dirty flags; a changed brightness goes to the gamma ramp | +| 0x0004fb70 | (set brightness) | Builds a ramp of `(BYTE)(i * brightness)` for all three channels and stores it into the device's pending ramp slot when it differs (an inlined `SetGammaRamp`) | + +Theseus adds TakeScreenShot, WireFrameStart/Stop and MotionStart/Stop as script functions. + +## Recovery + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0003ba60 | CRecovery::CreateNode | 0x2c bytes, everything zero | +| 0x0003bb20 | StartRecovery | Clears done, sets running, syncs the last percentage and starts the restore thread; if the thread can't start, progress becomes 1 and done is set | +| 0x0003bb70 | CRecovery::Advance | While running: done stops it and calls OnRecoveryComplete; a new percentage calls OnRecoveryProgressChanged, sets `recoveryProgress` to percent x 0.01 and resets the screen saver | +| 0x0002dc50 | FinishRecovery | `HalReturnToFirmware(1)` | + +Theseus has no disk restore to run, so StartRecovery always takes the finished path at 100%, and +desktop restarts the dashboard instead of rebooting. diff --git a/docs/decomp/Settings.md b/docs/decomp/Settings.md index b0731fa..6d0c907 100644 --- a/docs/decomp/Settings.md +++ b/docs/decomp/Settings.md @@ -41,3 +41,54 @@ The parser handles both Unicode and ANSI file formats at runtime. The `m_bUnicod ## XI_GetProgramPath() Utility function that resolves the directory containing the running XBE at runtime. Used to locate settings files relative to the dashboard's install path (e.g. `D:\Xbox.xbx` when running from the HDD, or the DVD path during development). Calls `XGetModuleFileName` and strips the executable name to get the directory. + +## CSettingsFile in the binary + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0003bc00 | CSettingsFile::Open | Copies the path, fetches the file with `CActiveFile` and widens it, then parses it | +| 0x0003bf00 | CSettingsFile::Save | Does nothing unless dirty; writes ANSI through `CreateFile` (`[name]\r\n`, `name=value\r\n`, each wide character narrowed) or Unicode through `_wfopen` and `fwprintf` (`[%s]\n`, `%s=%s\n`), then clears the dirty flag | +| 0x0003c100 | CSettingsFile::Close | Saves (stays open if that fails), frees the sections and the path | +| 0x0003c150 | CSettingsFile::FindSection | Linear search by name; with create set, pushes a new section on the front | +| 0x0003c1e0 | CSettingsFile::GetValue | Clears the output, tries the section then `default`, `wcsncpy` and terminates at size - 1 | +| 0x0003c260 | CSettingsFileSection::SetValue | Finds the name or pushes a new entry on the front; returns false when the value is unchanged | + +The object is 12 bytes: path, section list, dirty flag at +8 and the Unicode flag at +9. A section +and a value are each 12 bytes too (next, name, then the value list or the value). + +`Open` reads lines after `SkipWhite` with no comment style. `[name]` (up to 255 characters, and only +when the `]` is there) selects or creates a section. `name=value` lines (name up to 255, value up to +1023) go into the current section, creating `default` when none came first. Lines without `=` are +skipped. Overflowing any of the three buffers closes the file and fails. + +Theseus differences: the value buffer is 16384 characters (long cache.ini lines), saves go to +`.new` and replace the old file only when the write fully succeeded, writes can be batched +with `BeginBatch` / `EndBatch`, and `XI_GetProgramPath` and `OpenDir` are Theseus helpers. + +## The Settings and ScreenSaver nodes + +| Address | Name | What it does | +|---------|------|--------------| +| 0x000418a0 | CSettings::CreateNode | 0x30 bytes: `file`, `section`, then an embedded CSettingsFile at +0x24 | +| 0x00041920 | CSettings::~CSettings | Frees `file` and `section`, closes the settings file | +| 0x00041950 | CSettings::Dot | The node's own members first; any other name (a type 4 variable) becomes a 0x1c byte setting object holding the node and a copy of the name | +| 0x000419e0 | CSettings::SetValue | Reopens when no file is open or `file` differs ignoring case, sets the value in `section` and saves when it changed | +| 0x00041a50 | CSettings::GetValue | Same reopen (empty string if it fails), then reads the value into 1024 characters | +| 0x00041b60 | (setting) destructor | Frees the name | +| 0x00041b80 | (setting) Assign | `value->Deref()->ToStr()`, then `SetValue(name, text or "")` | +| 0x00041bc0 | (setting) Deref | `GetValue(name)`, then releases itself | +| 0x000400f0 | CScreenSaver::CreateNode | 0x38 bytes: enabled, nothing active, `delay` 300 (`0x0002c030`), the clock read from `0x0017e808`; registers itself at `0x0017e738` | +| 0x00040190 | CScreenSaver::~CScreenSaver | Clears the registration if it is this node | +| 0x000401b0 | CScreenSaver::Advance | See below | +| 0x000402a0 | reset | OnEnd if active, rearms stages 2 and 3, restarts the clock | +| 0x000402d0 | CScreenSaver::OnSetProperty | `enabled` set to true resets first; the value is stored either way | +| 0x00040320 | ResetScreenSaver | Stamps the global last event time (`0x0017c030`); with a ScreenSaver, resets it, calls `XAutoPowerDownResetTimer` and returns whether it was active | + +`Advance`: when disabled, an active saver ends with OnEnd. When enabled and idle, `delay` seconds +after the last event it starts with OnStart. Independently of `enabled`, `delay2` and `delay3`, +when above 0, fire OnDelay2 and OnDelay3 once each after that long. The comparisons are done in +float. + +The ScreenSaver's PRD (`0x001745d8`) is enabled, isActive, isActive2, isActive3, delay, delay2, +delay3 and its function table holds only `reset`. Theseus adds StartAfter, SetDelay2, SetDelay3 and +Settings.Reload. diff --git a/docs/decomp/ShapeRender.md b/docs/decomp/ShapeRender.md index 37f1d2f..2b22467 100644 --- a/docs/decomp/ShapeRender.md +++ b/docs/decomp/ShapeRender.md @@ -11,29 +11,308 @@ Shape rendering and vertex shader system in the 4920-5960 retail binary. | CMaterial | 0x00028734 | "Material" | 0x00028748 | | CSphere | 0x0002864c | "Sphere" | 0x0002865c | +Box is registered too: vtable `0x000285c0`, class object `0x001880f0`, prop map `0x00175130`. + +| Class | vtable | Factory | Size | +|-------|--------|---------|------| +| Shape | 0x000284a0 | 0x00050cf0 | 0x30 | +| Appearance | 0x00028500 | 0x00050f50 | 0x24 | +| Material | 0x00028560 | 0x00051300 | 0x90 | +| Sphere | 0x00028440 | 0x000529d0 | 0x50 | +| Box | 0x000285c0 | 0x000526c0 | 0x37c | + +Virtual slots used below: 4 Release, 6 GetNodeClass, 15 Advance, 16 Render, 17 GetBBox, +18 GetRadius, 22 GetTextureSurface, 23 (MeshNode) Init. + ## CShape Geometry + appearance container. Binds a geometry child (Sphere, mesh, etc.) to an Appearance node for rendering. +| Offset | Field | +|--------|-------| +| 0x1c | appearance | +| 0x20 | geometry | +| 0x24..0x2c | world position after the last draw | + +The destructor (`0x00050d60`) releases appearance, then geometry. + +**Render (`0x00050dd0`).** + +```c +if (geometry == NULL) return; +for (cls = geometry->GetNodeClass(); cls; cls = cls->base) // class chain walk + if (cls == &MeshNode_class /* 0x001883e8 */) { + mesh = geometry; + if (mesh->dirty) { + if (mesh->url && *mesh->url) Xap_load(mesh->url); + mesh->Init(); // slot 23 + if (mesh->core == NULL) return; + } + if (mesh->core == NULL || mesh->core->GetFVF() == 0) return; + g_pRenderMeshNode /* 0x0017e758 */ = mesh; + SetVertexShader(GetFixedFunctionShader(fvf)); // 0x0005c740 + break; + } +if (appearance) appearance->Render(); +geometry->Render(); +copy 16 dwords of the device's world matrix (pDevice + 0xde0); +position = row 3 (x, y, z); g_pRenderMeshNode = NULL; +``` + +**GetBBox (`0x00050eb0`)** tail calls the geometry's GetBBox, or zeroes all six floats. +**GetRadius (`0x00050ef0`)** returns the geometry's radius, or 0.0. +**Advance (`0x00050f10`)** runs the CNode advance, then geometry, then appearance. + ## CAppearance Material + texture binding. Links a CMaterial and texture references for a given shape. +Material at `0x1c`, texture at `0x20`. The destructor (`0x00050fc0`) releases them in that order. + +**Render (`0x00051030`).** Every device call is inlined into shadow state writes and push +buffer packets. Decoded, in order: + +| State | Value | +|-------|-------| +| CULLMODE | CCW (`0x901` in the Xbox encoding) | +| ZWRITEENABLE | 0 | +| ALPHABLENDENABLE | 1 | +| SRCBLEND | SRCALPHA (`0x302`) | +| DESTBLEND | INVSRCALPHA (`0x303`) | +| stage 0 COLOROP, ALPHAOP | DISABLE | +| stage 0 TEXCOORDINDEX | 0 | +| stage 0 TEXTURETRANSFORMFLAGS | 0 | +| stage 1 COLOROP, ALPHAOP | DISABLE | +| texture stages 0 and 1 | NULL | + +Then, with a texture node: its Render, then GetTextureSurface. With a surface it goes on stage 0 +and COLOROP/ALPHAOP become SELECTARG1 of TEXTURE. Without one ALPHAOP, ALPHAARG1, COLOROP, +COLORARG1 become SELECTARG1 of TFACTOR and TEXTUREFACTOR is set to 0, so the shape draws clear +until its image arrives. The material node renders last. + +**Advance (`0x000512c0`)**: CNode advance, material, texture. + ## CMaterial VRML97 material properties: diffuse color, emissive color, specular color, transparency. Standard Phong-style lighting parameters. +| Offset | Field | Default | +|--------|-------|---------| +| 0x1c | ambientIntensity | 0.2 | +| 0x20 | diffuseColor | 0.8, 0.8, 0.8 | +| 0x2c | emissiveColor | 0, 0, 0 | +| 0x38 | shininess | 0.2 | +| 0x3c | specularColor | 0, 0, 0 | +| 0x48 | transparency | 0 | +| 0x4c | D3DMATERIAL8 | | + +**Render (`0x000513a0`).** + +```c +a = 1.0 - transparency; +Diffuse = (diffuseColor, a); +Ambient = (ambientIntensity, ambientIntensity, ambientIntensity, a); +Specular = (specularColor, a); +Emissive = (emissiveColor, a); +Power = shininess; +copy 0x11 dwords to pDevice + 0xf00; dirty flags |= 0x9000; // SetMaterial, inlined +``` + +Material has no destructor of its own; its vtable points at the shared scalar deleting +destructor. + +## Box + +None of these are defined functions in the project database; they read straight from the +disassembly. + +| Offset | Field | +|--------|-------| +| 0x1c | size, default (1, 1, 1) | +| 0x28 | vertex buffer | +| 0x2c | index buffer | +| 0x30 | 24 vertices, 32 bytes each (position, normal, uv) | +| 0x330 | 36 WORD indices | +| 0x378 | dirty, starts 1 | + +**Render (`0x00052820`).** + +1. If dirty, build the cube into the node's own arrays (`0x00051450`, outside facing) and clear dirty. +2. With no vertex buffer, create a 0x300 byte one through the shared create helper + (`0x0003d410`, which frees mesh and texture caches and retries until it gets memory) and copy + the vertices in with `rep movsd`. +3. Same for the index buffer, 0x48 bytes (`0x0003d390`). +4. With both: SetStreamSource(0, vb, 0x20), SetIndices(ib, 0), SetVertexShader(0x112), then + DrawIndexedVertices(TRIANGLELIST, 36, indices). + +**GetBBox (`0x00052960`)**: center (0, 0, 0), size as set. +**GetRadius (`0x00052990`)**: `sqrt(x*x + y*y + z*z) * 0.5`, in x87. +**Destructor (`0x00052750`)**: releases the vertex buffer, then the index buffer. + +### The cube builder (`0x00051450`) + +Fastcall with the inside flag in CL, vertices in EDX and indices in EAX, size by value on the +stack. Fully unrolled: each vertex is assembled in a 32 byte stack record and copied out with +`rep movsd`. Constants: `0.5` (`0x0002bed4`), `-0.5` (`0x0002bedc`), `1.0` (`0x0002bed8`), +`-1.0` (`0x0002bef0`), `0.01` (`0x0002bfe8`), `0.99` (`0x0002bfa8`). + +Every position is `+-0.5 * size` per axis, texture coordinates are inset to 0.01 and 0.99, and the +faces come in the order -z, +z, +y, -y, +x, -x with their outward normals. The outside facing +corners (sign per axis, then u and v as low 0 / high 1): + +| Face | Corner 0 | Corner 1 | Corner 2 | Corner 3 | +|------|----------|----------|----------|----------| +| -z | - - - / 0 0 | + - - / 1 0 | + + - / 1 1 | - + - / 0 1 | +| +z | + + + / 0 1 | + - + / 0 0 | - - + / 1 0 | - + + / 1 1 | +| +y | - + - / 0 0 | + + - / 1 0 | + + + / 1 1 | - + + / 0 1 | +| -y | + - + / 1 1 | + - - / 1 0 | - - - / 0 0 | - - + / 0 1 | +| +x | + - - / 0 0 | + - + / 1 0 | + + + / 1 1 | + + - / 0 1 | +| -x | - + + / 0 1 | - - + / 0 0 | - - - / 1 0 | - + - / 1 1 | + +The inside facing branch writes the same four corners of each face in reverse order and keeps +the normals. The indices are `0, 1, 2, 2, 3, 0` per face, offset by 4 each face. Box only ever +calls it outside facing. + ## CSphere Procedural sphere mesh generator. Builds vertex/index buffers at runtime from a radius parameter. +A MeshNode (its constructor is inlined) with radius 1.0 at `0x40`, slices 32 at `0x44` and stacks +32 at `0x48`, created dirty. Its destructor (`0x00052a90`) is only the MeshNode one. + +**Init (`0x00052aa0`, MeshNode slot 23).** + +```c +core = MakeSphere(radius, slices, stacks); // 0x0005c390: D3DXCreateSphere + D3DXComputeNormals +if (core) dirty = 0; +``` + ## Vertex Shader System The binary contains vertex shader management for both effect shaders and a fixed-function fallback path. Compressed normals are supported (packed into a single DWORD, expanded in the vertex shader). -Falloff and reflection shader setup is present for environment-mapped surfaces. +Both lookups use 0x14 byte entries: effect number, microcode pointer, FVF, shader handle (0 until +created), and a spare dword. + +**Effect table (`0x00175bb0`, 12 entries).** + +| Effect | Microcode | FVF | +|--------|-----------|-----| +| 1 | 0x001754f0 | 0x20000042 (XYZ, packed normal, diffuse) | +| 1 | 0x001754f0 | 0x20000002 | +| 1 | 0x00175648 | 0x20000102 (with one texture coordinate) | +| 2 | 0x00175ad0 | 0x20000002 | +| 3 | 0x001757a0 | 0x20000102 | +| 4 | 0x001758f8 | 0x20000002 | +| 1 | 0x001754f0 | 0x52 (XYZ, float normal, diffuse) | +| 1 | 0x001754f0 | 0x12 | +| 1 | 0x00175648 | 0x112 | +| 2 | 0x00175ad0 | 0x12 | +| 3 | 0x001757a0 | 0x112 | +| 4 | 0x001758f8 | 0x12 | + +**Fixed function table (`0x00175b74`, 3 entries)**: FVFs `0x20000042`, `0x20000002`, +`0x20000102`, all pointing at the `0x001754f0` microcode, which is never used for them. + +**Microcode.** Five NV2A programs. The first word is `0x2078` with the instruction count in the +high half, then four words per instruction. + +| Address | Instructions | Used for | +|---------|--------------|----------| +| 0x001754f0 | 21 | effect 1 | +| 0x00175648 | 21 | effect 1, textured | +| 0x001757a0 | 21 | effect 3 | +| 0x001758f8 | 29 | effect 4 | +| 0x00175ad0 | 10 | effect 2 | + +**Declarations.** + +| Address | Tokens | +|---------|--------| +| 0x00176660 | stream 0; v0 FLOAT3; v2 NORMPACKED3; v3 D3DCOLOR; end | +| 0x00176674 | stream 0; v0 FLOAT3; v2 NORMPACKED3; end | +| 0x00176688 | stream 0; v0 FLOAT3; v2 NORMPACKED3; v9 FLOAT2; end | +| 0x0017669c | stream 0; v0 FLOAT3; v3 FLOAT3; v6 FLOAT2; end | +| 0x001766b0 | stream 0; v0 FLOAT3; v3 FLOAT3; end | +| 0x001766c0 | stream 0; v0 FLOAT3; v3 NORMPACKED3; v6 FLOAT2; end | +| 0x001766d4 | stream 0; v0 FLOAT3; v3 NORMPACKED3; end | + +The first three are the fixed function ones, 0x14 bytes apart and indexed by table position. + +**GetEffectShader (`0x0005c6c0`).** Effect in ECX, FVF in EAX. + +```c +if (effect == 0) return fvf; +for (i = 0, e = table; e->effect != effect || e->fvf != fvf; i++, e++) + if (i >= 11) return fvf; +if (e->shader == 0) { + if (fvf & 0x20000000) decl = (fvf & 0x100) ? 0x1766c0 : 0x1766d4; + else decl = (fvf & 0x100) ? 0x17669c : 0x1766b0; + CreateVertexShader(decl, e->microcode, &e->shader, 0); // 0x00147cc0 +} +return e->shader; +``` + +**GetFixedFunctionShader (`0x0005c740`).** FVF in EAX. Walks the three entries for a matching FVF +and returns the FVF itself when none matches. On first use it builds the shader inline from the +declaration at `0x00176660 + i * 0x14` with no microcode: size the declaration's constants, +XMemAlloc `0x118` plus that, zero it, mark 16 attribute slots, parse the declaration in, and +store the pointer with bit 0 set as the handle. + +## CompressNormal (`0x0005c820`) + +The vector comes in EAX. The length is computed in x87 (`fsqrt`), its reciprocal in SSE, and each +component multiplied by it. Then `ftol2` of `x * 1023`, `y * 1023` (`0x0002c014`) and `z * 511` +(`0x0002c010`): + +```c +return (ftol(x * 1023) & 0x7ff) | ((ftol(y * 1023) & 0x7ff) << 11) | (ftol(z * 511) << 22); +``` + +`ftol2` truncates through a signed 64-bit integer, so negative components land in the packed +fields as two's complement. + +## Shader Constants + +Constant registers are passed to `D3DDevice_SetVertexShaderConstant1/4` biased by 96, so `0x88` +is c40 and `0x60` is c0. The world matrix is the top of the matrix stack (`0x0017e7fc`, vtable +slot `0x44`), view is `0x0017f468`, projection `0x0017f4e8`. + +Both setters end with the same light direction: `(1, 1, -1, 0)` normalized, transformed by the +transpose of world * view with D3DXVec3TransformNormal, normalized again. + +**SetReflectShaderFrameValues (`0x0005c8d0`).** + +| Register | Value | +|----------|-------| +| c40..c43 | transpose(world * view) | +| c44..c47 | transpose(projection) | +| c48 | (0, 0, 1, 0.5) | +| c49 | light direction | + +Then LIGHTING goes to FALSE (inlined state write). + +**Falloff frame values (`0x0005ca00`).** Runs whenever the world matrix changes. + +| Register | Value | +|----------|-------| +| c0..c3 | transpose(world * view * projection) | +| c10..c13 | transpose(world * view) | +| c5..c8 | inverse(world * view) | +| c9 | (0, 0.5, 1, -1) | +| c4 | light direction | + +Then texture stage 0 is set to NULL and LIGHTING to FALSE, both inlined. + +**Falloff colors.** Not a function of its own: every falloff family material Setup inlines it, +for example CFalloffMatInfo_Setup (`0x00057790`): + +```c +c15 = (side.r, side.g, side.b, side.a * effectAlpha); +c16 = (front.r - side.r, front.g - side.g, front.b - side.b, (front.a - side.a) * effectAlpha); +``` -The global `g_nEffectAlpha` controls scene-wide transparency for fade transitions. +The scene-wide fade `effectAlpha` lives at `0x00175ca0` and starts at 1.0. ## Mesh Normal Smoothing diff --git a/docs/decomp/StringObject.md b/docs/decomp/StringObject.md index e794217..279abcf 100644 --- a/docs/decomp/StringObject.md +++ b/docs/decomp/StringObject.md @@ -24,3 +24,29 @@ The string type is effectively immutable for script purposes. All methods that p ## Implementation Notes `_tcslwr` and `_tcsupr` are the Xbox CRT's locale-aware case conversion functions (TCHAR variants). These operate in-place on the internal buffer of the newly-created return string, not on the caller's string. + +## Binary layout and functions + +The object is 0x20 bytes: the CObject header (type 3 at +0x08), then length at +0x14, capacity at ++0x18 and the UTF-16 buffer at +0x1c. + +| Address | Name | What it does | +|---------|------|--------------| +| 0x00032040 | CStrObject() | Type 3, no buffer | +| 0x00032090 | CStrObject(sz) | `wcslen`, a buffer of length + 1, copy; NULL reads as `""` | +| 0x00032130 | CStrObject(pch, cch) | A buffer of cch + 1, copy cch characters, terminate | +| 0x000321d0 | ~CStrObject | Frees the buffer | +| 0x00032200 | ToNum | `wcstod` of the text into a new number object | +| 0x00032260 | Append | Grows to exactly length + n + 1 when needed, copies onto the end | +| 0x00032300 | charCodeAt | The UTF-16 unit, -1 out of range | +| 0x00032320 | charAt | A one character string; no object at all out of range | +| 0x000323a0 | concat | A new string sized for both | +| 0x00032460 | indexOf | 1 or 2 arguments; start clamped to 0..length; `wcsstr` from there when start is short of the end | +| 0x00032550 | (reverse search) | From text + start - len(find) backwards with `wcsncmp`; returns where it stopped, so a miss can give an index below -1 | +| 0x000325b0 | lastIndexOf | Same argument rules, start defaults to the length | +| 0x00032680 | substr | count defaults to length - start; start capped at length; count of 0 or less is empty; count past the end stops there. A negative start is not clamped | +| 0x00032780 | substring | Exactly 2 arguments; negative start is 0, negative end is start; swapped when out of order; both capped at length | +| 0x00032850 / 0x000328c0 | toLowerCase / toUpperCase | A copy, then `wcslwr` / `wcsupr` | + +A wrong argument count raises "invalid number of parameters" (string at `0x00021b38`) and returns +nothing. Theseus answers `""` from an out of range `charAt` and -1 from every missed `lastIndexOf`. diff --git a/docs/decomp/Text.md b/docs/decomp/Text.md index 944cbba..3684877 100644 --- a/docs/decomp/Text.md +++ b/docs/decomp/Text.md @@ -1,17 +1,328 @@ # Font & Text Rendering -Infrastructure layer; no XAP node class. This is not a script-accessible node type. +Vector fonts, text meshes and the `Text` node in the 5960 retail binary. Glyphs are stored +pretriangulated in `.xtf` files; a string becomes one D3DX mesh, which the node draws and +scrolls. -## Font Loading +## Functions -Loads Xbox .xtf font files. A font table is built at startup by scanning the font directory. FindFont and GetFont map face name strings to loaded font file handles. +| Address | Name | What it does | +|---------|------|--------------| +| 0x000537c0 | (XTF1 setup) | Creates the LZX decompressor and the block cache for compressed fonts | +| 0x00053840 | (font read) | `ReadFile` wrapper that refuses anything over 0x1400000 bytes | +| 0x00053870 | CFont::Close | Closes the file, frees loaded shapes and every table | +| 0x00053970 | (XTF0 open) | Reads the header, glyph set and glyph records | +| 0x00053b00 | (XTF1 open) | Same, plus a per glyph offset table | +| 0x00053ce0 | CFont::Open | Opens the file, verifies it, dispatches on the magic | +| 0x00053d80 | (XTF1 shape parse) | Copies one glyph shape out of a decompressed block | +| 0x00053ed0 | CFont::LoadGlyph (XTF0) | Seeks to a glyph shape and reads it | +| 0x00054010 | CFont::LoadGlyph (XTF1) | Finds or decompresses the block holding a glyph | +| 0x000541c0 | CFont::FindGlyphIndex | Code point to glyph index | +| 0x00054290 | HorizontalFade | Fades vertex alpha out toward a column edge | +| 0x00054380 | VerticalFade | Fades vertex alpha at the top and bottom of a scroll window | +| 0x00054440 | CFont::CreateTextMesh | Lays out a string into a mesh | +| 0x00055030 | CFont::CreateCursorMesh | Appends the blinking cursor quad | +| 0x000552b0 | InitFontTable | Scans the font directory | +| 0x00055640 | GetFont | Face name to an open font | +| 0x00055750 | Text_Exit | Frees the font table | +| 0x000557b0 | CTextNode::CreateNode | Factory for `Text` | +| 0x000557e0 | CTextNode::CTextNode | Constructor | +| 0x00055880 | CTextNode::~CTextNode | Destructor | +| 0x00055900 | CTextNode::Advance | Vertical scroll | +| 0x000559a0 | FaceFromFont | `body` / `heading` to a face name | +| 0x000559e0 | CTextNode::Render | Builds and draws the mesh | +| 0x00055f20 | CTextNode::OnSetProperty | Drops the mesh on any property but `scroll` | +| 0x00055f60 | FindText | Ten slot cache of throwaway text nodes | -## Text Mesh Generation +Globals: the keyboard's string `0x0017e748`, cursor index `0x00175220` (starts -1), last built +cursor `0x00175224`, the asterisk formatting flag `0x0017e74c` (a BSS byte, so it starts off; +only the keyboard's `SetAsteriskFormattingState` / `GetAsteriskFormattingState` touch it), the +font table `0x001763e8` (ten 0x30 byte slots: face, file, then the CFont) and its count +`0x00175228` (starts 1, slot 0 is `HGGothicM` / `HGGothicM.xtf`). -Builds text meshes from glyph outlines by triangulating Bezier curves. Glyph data is cached with LRU eviction to keep memory usage bounded. +## The XTF file -## Rendering Features +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | magic, `XTF0` (`0x30465458`) or `XTF1` (`0x31465458`) | +| 4 | 4 | header length, 0x20 | +| 8 | 0x20 | face name, ASCII | +| 0x28 | 4 | glyph set size `cb` | +| 0x2c | cb - 4 | glyph set: `flAccel`, glyph count, range count, then (first code point, count) WORD pairs | +| | count * 0x1c | glyph records: six floats (black box x, y, origin x, y, advance x, y) and the shape's file offset | -- Vertical fade (alpha gradient top-to-bottom) -- Alpha blending -- Multi-line layout +A glyph shape is a WORD index count, a WORD point count, the indices, then the points as float +(x, y) pairs. In memory a shape is 0xc bytes: the two counts, then pointers to the index and point +arrays. The record's offset dword is overwritten with the shape pointer once loaded. + +The CFont object: file handle `+0`, magic `+4`, glyph set `+8`, glyph records `+0xc`, loaded flags +`+0x10`, then XTF1 only: per glyph block offsets `+0x14`, decompressor `+0x18`, largest +compressed block `+0x1c`, compressed buffer `+0x20`, block cache `+0x24`. + +## Opening a font + +**Open (`0x00053ce0`)** opens the file read only (`GENERIC_READ`, `FILE_SHARE_READ`, +`OPEN_EXISTING`), then checks it against the dash's signed file hash table (`0x0003eae0`, which +calls `HalReturnToFirmware` on a mismatch). It reads the magic: `XTF0` goes to `0x00053970`, +`XTF1` sets up decompression (`0x000537c0`: `LDICreateDecompression` with 0x8000 byte blocks and +a 0x20000 window, a compressed buffer of the size it reports, and a zeroed ten block cache) and +goes to `0x00053b00`; anything else closes the file and fails. + +**XTF0 body (`0x00053970`).** + +```c +read 4 (header length, not checked here); read 0x20 (face); read 4 (cb); +if (cb < 0x14) fail; +set = new(cb); set->cbThis = cb; read cb - 4 into set + 4; +if (bytes read != cb - 4 || set->cRanges * 4 + 0x10 != cb) fail; +glyphs = new(set->cGlyphs * 0x1c); read them; check the byte count; +loaded = new(set->cGlyphs); zero it; +``` + +Every read checks both the return value and the byte count. A failure returns false without +closing anything; Close cleans up later. The XTF1 body reads a WORD per glyph before the records: +where the glyph's shape sits inside its decompressed block. The record's offset then points at +the compressed block. + +**LoadGlyph, XTF0 (`0x00053ed0`).** + +```c +if (loaded[n]) return 1; +off = glyphs[n].shape; if (off == 0) return 0; +if (SetFilePointer(hFile, off, 0, FILE_BEGIN) == -1) return 0; +s = new(0xc); +read 2 -> s->nIndices; s->indices = new(nIndices * 2); +read 2 -> s->nPoints; s->points = new(nPoints * 8); +read the indices, then the points, each through the capped reader and size checked; +glyphs[n].shape = s; loaded[n] = 1; return 1; +``` + +**LoadGlyph, XTF1 (`0x00054010`)** keeps ten decompressed blocks (0x8008 bytes each: last use +tick, block offset, data). A hit refreshes the tick; a miss takes the oldest slot, reads the +block's WORD compressed and decompressed sizes (no more than the largest compressed block and +0x8000), the compressed bytes, runs `LDIDecompress` and checks the output size. `0x00053d80` then +copies the shape out of the block, bounds checking every read against 0x8000. + +**FindGlyphIndex (`0x000541c0`).** + +```c +for (;;) { + base = 0; + for (i = 0; i < set->cRanges; i++) { + if (r[i].first <= ch && ch < r[i].first + r[i].count) return ch - r[i].first + base; + base += r[i].count; + } + if (ch == 0x7f) return 0; + ch = 0x7f; // anything the font lacks draws as the DEL box +} +``` + +**Close (`0x00053870`)** closes the handle, frees each loaded, non-null shape (indices, points, +shape), destroys the decompressor, then frees the glyph set, records, loaded flags, block table, +compressed buffer and block cache. + +## Font table + +**InitFontTable (`0x000552b0`).** Builds `fonts\*.xtf` and runs `FindFirstFileA` on it, +falling back to `*.xtf`. For each hit, while the count is under ten: strip a `file:` +prefix and turn `/` into `\`, open, read the magic (`XTF0` or `XTF1`), the header length (must be +0x20) and the 32 byte face. The face is widened; if it equals slot 0's face it's freed, otherwise +the face and a copy of the full path go into the next slot. A bad header skips the file without +closing it. + +**GetFont (`0x00055640`).** + +```c +if (count == 1) InitFontTable(); +for (i = 0; i < count; i++) if (!_wcsicmp(face, fonts[i].face)) break; +if (i == count) i = 0; +if (fonts[i].font.hFile == -1) { + path = wcschr(file, ':') ? file : swprintf(L"%s%s", appdir, file); + if (!Open(path) && fonts[0].font.hFile == -1 && !Open(fonts[0].file)) return NULL; +} +return &fonts[i].font; +``` + +**Text_Exit (`0x00055750`)** frees the face and file strings of every slot past the first and +closes each font. + +**FaceFromFont (`0x000559a0`)**: NULL or `body` gives `XBox Book`, `heading` gives `Xbox`, +anything else passes through. + +## Text meshes + +Vertices are FVF `0x52` (XYZ, NORMAL, DIFFUSE), 28 bytes: position, normal (0, 0, 1), color. +Constants: `1.0` (`0x0002bed8`), `0.5` (`0x0002bed4`), `2.0` (`0x0002bef4`), `3.0` +(`0x0002bf00`), `255.0` (`0x0002bf38`), `1.5` (`0x0002c184`), `0.1` (`0x0002bfc0`), `0.75` +(`0x0002c018`), `pi` (`0x0002bffc`), `16.556152` (`0x0002c1b4`). + +**CreateTextMesh (`0x00054440`)** takes the font, the string, its length (-1 for `wcslen`), the +mesh out pointer, bounding box min and max, a format width, a don't-break flag, a scale and an +indent (width, height); `ret 0x2c`. + +A heading is a column that starts and ends with `*` (at least two characters before the tab or +line end). Every heading test also requires the asterisk formatting flag. + +First pass, per line: + +1. If the line opens a heading, skip the `*` and remember it. The memory isn't cleared at the next + line, only at a tab. +2. Per character: a remembered heading's closing `*` (next character is tab, line end or the end) + is skipped. Without don't-break, `\r` (eating a following `\n`) and `\n` end the line, and a tab + switches to column 2, marks the text as multi column and checks for a heading after it. +3. Otherwise look the glyph up and load it (XTF0 or XTF1 loader by magic). A glyph that won't load + has its record's shape slot zeroed and is skipped. +4. If the vertex total would pass 0xffff, the string is cut at this character and the pass ends. +5. Add the glyph's advance to the column's width, index count / 3 to the face count and its points + to the vertex count. + +The widest first and second columns are kept. A cursor index from 0 to the length adds two faces +and four vertices. No faces means no mesh. + +Then with a positive format width: a single column takes the whole width; two columns wider than +it shrink the first to `width - col2` and turn fading on. Without a width fading is off; otherwise +fading starts as the don't-break flag. + +The mesh create helper (`0x0003d2f0`) makes it with D3DXMESH_MANAGED (0x220) and FVF 0x52, freeing +caches and retrying on out of memory. Then both buffers are locked and written without checks. + +Second pass. Color is white with the blue byte from a float that headings set to 0.5 and their +closing `*` sets back to 1.0: `0xffffff00 | (int)(blue * 255)`. x starts at the indent width. +Per line, column 1 with the first column width as its limit; per character: + +- Past the end reads as `\r`. `\r` eats a following `\n`. +- Line end: with don't-break and fading, fade the column (`limit - 1` to `limit`) and keep going on + the same line. Otherwise column 2 shifts right by `col2 max - col2 width`, column 1 fades if + fading is on, and unless this is the end of the string y drops 1.0 (in a heading, after `\r`, or + with two columns) or 1.5 (a new paragraph), with x going back to 0 below the indent height and to + the indent width inside it. +- Tab (without don't-break): fade the column from `limit - 2` if fading, then column 2 starts at + `width - col2 max` with that as its limit, and a heading may open after it. +- A heading's closing `*` restores blue to 1.0 and is skipped. +- With a positive width, without don't-break, in column 1, a non space glyph that would end past + the width wraps: y drops 1.0, x goes back to 0 or the indent, and the string, vertex and index + positions rewind to the last break point. With no break point on the line fading turns on + instead. +- A glyph with no shape is skipped. Otherwise its indices go in offset by the vertex position and + its points go in at (x + px, y + py). +- If the character just read is at the cursor index, the cursor quad goes in at (x, y). +- x advances by the glyph's advance (column 2 also adds it to its width). A space, anything from + 0x3040 up to 0xf000, or 0xff66 and up becomes the new break point. + +A cursor index equal to the length gets the quad after the last character, visible only while +x < 16.556152 (the edge of the keyboard's edit box). Then every counted vertex is scaled (x, y, +z) and, given both pointers, `D3DXComputeBoundingBox` runs over the written vertices. + +**HorizontalFade (`0x00054290`).** Vertices in EAX, end in XMM1, direction in BL, then count and +start on the stack. + +```c +inv = 1.0 / (end - start); +for (i < n) { + a = (v.x - start) * inv; + a = a <= 0 ? 0 : a >= 1 ? 1 : (3 - 2a) * a * a; + if (out) a = 1 - a; + v.alpha = (int)(v.alpha * a); +} +``` + +CreateTextMesh only ever calls it fading out. + +**VerticalFade (`0x00054380`).** Vertices in ECX, count in EDX, top in XMM4, bottom in XMM5, +scroll on the stack. + +```c +y = v.y + scroll; +if (y > top || y < bottom) a = 0; +else if (y > top - 1) a = 1 - (y - (top - 1)); +else if (y < bottom + 1) a = 1 - ((bottom + 1) - y); +else a = 1; +alpha byte = (int)(ease(a) * 255); // same ease as above +``` + +**CreateCursorMesh (`0x00055030`).** + +```c +a = |sin((now + now) * pi)|; if (!visible) a = 0; // x87, now is the double clock +color = (int)(a * 255) << 24 | 0xffffff; +indices += { v, v+1, v+2, v, v+2, v+3 }; +(x, y - 0.1) (x + 0.1, y - 0.1) (x + 0.1, y + 0.75) (x, y + 0.75), normal (0, 0, 1) +``` + +## CTextNode + +0x78 bytes. Prop map `0x00175230`, vtable `0x00028928`. + +| Offset | Field | Default | +|--------|-------|---------| +| 0x1c | text | | +| 0x20 | font | | +| 0x24 | justify | | +| 0x28 | deviation | 0.01 | +| 0x2c | extrusion | 1.0 | +| 0x30 | width | 0 | +| 0x34 | translate | 1 | +| 0x38 | height | 0 | +| 0x3c | scroll | 0 | +| 0x40 | scrollRate | 0 | +| 0x44 | scrollDelay | 0 | +| 0x48 | indentWidth | 0 | +| 0x4c | indentHeight | 0 | +| 0x50 | mesh | | +| 0x54 | bbox min, max | | +| 0x6c | language the mesh was built in | 0 | +| 0x70 | time scrolling starts (double) | 0 | + +**Destructor (`0x00055880`)** frees the three strings and releases the mesh. + +**Advance (`0x00055900`).** + +```c +CNode::Advance(t); +if (scrollRate > 0 && mesh) { + if (timeToScroll == 0) timeToScroll = scrollDelay + now; + h = bbox.max.y - bbox.min.y; + if (timeToScroll <= now && h > height) { + scroll = height * t * scrollRate + scroll; + if (h + 1.5 <= scroll) scroll -= h + 1.5; + } +} +``` + +**Render (`0x000559e0`).** + +1. If the keyboard left a string: when the text, or the cursor against the last built cursor, + differs, copy it in, record the cursor and rebuild. A cursor of -1 always rebuilds. Clear the + string. +2. No text, or empty: return. +3. `` and `` prefixes: format the local time (`FormatTime`) or date (`FormatDate`, + `0x0003cd40`) into a 32 character buffer; if it differs from what follows the prefix, the text + becomes the prefix plus the value and the mesh rebuilds. The node stops translating and the + string after the prefix is what gets drawn. +4. Translating and the current language differs from the mesh's: rebuild. Rebuild releases the + mesh. +5. No mesh: translate if translating, record the language, then + `CreateTextMesh(GetFont(FaceFromFont(font)), text, -1, &mesh, &min, &max, |width|, width < 0, + 1.0, indentWidth, indentHeight)`. Still no mesh: return. +6. Justify `middle` or `end` offsets x by half or all of the drawn width: the bounding box width, + capped at |width| when width is set. +7. Twice: if height is set and the text is taller, lock the vertices and `VerticalFade` with top + `1 - scroll` (one higher while scroll is 0), bottom `top - height - 2` and, for the second + copy, an offset of `-(h + 1.5)`; the second copy is skipped while `height + scroll < h`. + Without scrolling there's no second copy. Each draw pushes the world matrix, translates by + (x offset, scroll, plus `-(h + 1.5)` for the second copy), loads it into the device, sets + CULLMODE to NONE, `DrawSubset(0)`, CULLMODE back to CCW, pops and reloads the world matrix. + +**OnSetProperty (`0x00055f20`).** Any property but `scroll` (offset 0x3c) releases the mesh and +zeroes the scroll start time and the scroll. + +**FindText (`0x00055f60`).** Ten (node, last used) slots. + +```c +for (i < 10) { + if (node && node->text && !wcscmp(node->text, s) && node->width == w) { used = now; return slot; } + if (!empty) { if (!node) empty = slot; else if (!oldest || used < oldest->used) oldest = slot; } +} +if (!empty && oldest) { delete oldest->node; zero the slot; empty = oldest; } +empty->node = new CTextNode; node->text = copy(s); node->width = w; empty->used = now; +``` diff --git a/docs/decomp/TmapSystem.md b/docs/decomp/TmapSystem.md index ccf38fe..87f5706 100644 --- a/docs/decomp/TmapSystem.md +++ b/docs/decomp/TmapSystem.md @@ -13,6 +13,7 @@ All node type strings confirmed in the 4920-5960 retail XBE. | ImageFader | 0x0002a1e0 | 0x0002a1c8 "CImageFader" | | AudioVisualizer | 0x0002a9e8 | 0x0002a9c4 "CAudioVisualizer" | | DotField | 0x0002a490 | 0x0002a47c "CDotField" | +| StarField | | factory `0x00060390`, vtable `0x0002a368` | ## Properties @@ -45,7 +46,13 @@ All node type strings confirmed in the 4920-5960 retail XBE. ### CDotField -A particle effect rendered into the dynamic texture pipeline. Renders a field of dots that respond to audio levels and time. Used for the dashboard background animation. +- `spacing` (pt_number): grid step in pixels (default 8) +- `hSpeed`, `vSpeed` (pt_number): scroll speed; both 0 means wander on its own + +### CStarField + +- `stars` (pt_integer): how many stars (default 20) +- `speed` (pt_number): depth units per second (default 1.0) ## Architecture @@ -58,23 +65,424 @@ The rendering pipeline works as: 5. The 8-bit buffer is swizzled into the Xbox texture via the Swizzler API. 6. CPalette uploads a 256-entry hardware palette via D3DPalette. -## Palette Presets (10 styles) +## CSurfx (8-bit surface) + +A 0x24 byte object, allocated inline by the DynamicTexture update at `0x0005dde0`. + +| Offset | Field | +|--------|-------| +| 0x00 | width | +| 0x04 | height | +| 0x08 | pixels, `new[(height + 1) * width]` (one spare row) | +| 0x0c | bytes per row (= width) | +| 0x10 | line width, always 1 | +| 0x14..0x20 | clip rect `{0, 0, width, height}` | + +Constructor `0x0005f3f0` bumps a live count at `0x0017e75c`. The DynamicTexture destructor +(`0x0005dbc0`) frees the pixels, drops the count, and when it hits zero frees the shared fade +scratch buffer (`0x0017e764`, size at `0x0017e760`). + +### Delta cells + +A delta field is 3 bytes per pixel, read as a 32-bit load then masked. Bits 12..23 hold the +source x offset, bits 0..11 the source y offset, both in 1/32 pixel steps and biased by 63 +pixels (`0x7e0`). `0x7e07e0` means "this pixel pulls from itself". + +### Fade, out of place (`0x0005f430`) + +```c +src += -0x3f * srcPitch - 0x3f; +for (row...) { + for (x = 0; x < w; x++) { + code = *(uint *)grad; grad += 3; + p = src + x + ((code >> 12 & 0xfff) >> 5) + ((code & 0xfff) >> 5) * srcPitch; + fx = code >> 12 & 0x1f; fy = code & 0x1f; + dest[x] = ((p[pitch]*fy + p[0]*(32-fy))*(32-fx) + (p[pitch+1]*fy + p[1]*(32-fy))*fx) * 0x1f >> 0xf; + } + dest += destPitch; src += srcPitch; +} +``` + +Bilinear 5-bit weights total 1024, so `* 31 >> 15` is the blend times 31/32: every pass dims +the image a little, which is what makes trails fade out. + +### Fade, in place (`0x0005f540`) + +Takes the DeltaFieldData (`{negY rows, cells}`), the pixels, pitch, width and height. + +1. Grows the shared scratch buffer to `negY * pitch` if needed (free, then new). +2. Clamps the ring height to the image height. +3. Runs the out-of-place pass for the first `negY` rows into the scratch buffer. +4. For each later row: copies the oldest scratch row back into the image row `negY` above, + then computes the current row into that scratch slot (`slot = (slot + 1) % negY`). +5. Flushes the remaining scratch rows in order, `width >> 2` dwords per row. + +A pixel can pull from at most `negY` rows above, so a row only goes back into the image once +nothing still needs the old values. + +Quirks: the bilinear tap reads one row and one pixel past the source pixel, so the bottom-right +pixel reads one byte past the image (the spare row covers everything else). A y offset of more +than 63 rows up underflows its 12-bit field and borrows from the x field. + +## Line (`0x0005fdf0`) + +`__fastcall`, with the surface and color on the stack. + +- Each coordinate is folded as `((v >> 1 ^ v) & 0x3fffffff) ^ (v >> 1)`: low 30 bits kept, sign + copied into bits 30 and 31. +- Line width 4 and up is scaled by direction: with squared run lengths `dx2`, `dy2`, + `w = (lineWidth * (128 + 55 * minor / major) + 64) >> 7`, so diagonals get up to 1.43x. +- Clipping: the clip rect is inset by `w >> 1`. If the start is outside, the ends swap; if the + end is outside too, nothing is drawn. Step counts are trimmed so the walk stops one short of + the right and bottom edges. +- Width 1: integer Bresenham, one pixel per step, `x`-major when `dx >= dy`. +- Wider: first a round cap at the start, then per step a `w` pixel span across the minor axis + (`x`-major when `dx > dy`). Cap insets per row come from byte tables: + +| Width | Table | Insets | +|-------|-------|--------| +| 2 | 0x0002a364 | 0 0 | +| 3 | 0x0002a360 | 1 0 1 | +| 4 | 0x0002a358 | 1 0 0 1 | +| 5 | 0x0002a350 | 1 0 0 0 1 | +| 6 | 0x0002a348 | 1 0 0 0 0 1 | +| 7 | 0x0002a340 | 2 1 0 0 0 1 2 | +| 8 | 0x0002a334 | 2 1 0 0 0 0 1 2 | +| 9 | 0x0002a328 | 3 1 1 0 0 0 1 1 3 | +| 10 | 0x0002a31c | 3 1 1 0 0 0 0 1 1 3 | +| 11 | 0x0002a310 | 4 2 1 1 0 0 0 1 1 2 4 | + + From 12 up the inset is computed: `half - (int)fsqrt(half*half - k*k)` for `k = -half..`. + +Nothing in the 5960 dash changes the line width from 1, so only the 1 pixel path runs. + +## DeltaField + +A 0x3c byte object built inline by ImageFader (`0x0005e120`). + +| Offset | Field | +|--------|-------| +| 0x00 | style | +| 0x04 | current row (-1 = never sized) | +| 0x08 | most negative y offset so far (1/32 px) | +| 0x0c..0x18 | x, y, r, theta of the cell being computed | +| 0x1c, 0x20 | x scale, y scale | +| 0x24, 0x25, 0x26 | polar, has r term, has theta term | +| 0x28, 0x2c | width, height | +| 0x30 | aspect 1:1 | +| 0x34, 0x38 | DeltaFieldData: ring rows, cells | + +**Assign + SetSize (`0x0005e4e0`, also inlined twice in ImageFader).** Sets aspect, polar, r and +theta flags to 1, frees and reallocates the cells as `w * h * 3 + 0x40`, sets +`xs = 2.0 / w`, `ys = 2.0 / h`, makes both the larger of the two when aspect is on, then +`ys *= -1.0` (`0x0002bef0`) so +y is up. Resets the row and the negative extent to 0. + +**CalcSome (`0x0005e570`).** One row per call (two when starting on row 0). Top and bottom rows +and the first and last cell of every row are `0x7e07e0`. For the rest: + +```c +y = (h - row*2) * ys * 0.5; fx = 32.0 / xs; fy = 32.0 / ys; +x = (col*2 - w) * xs * 0.5; +if (hasR) r = sqrt(x*x + y*y); +if (hasTheta) theta = fpatan(y, x); +GetXY(init = (col == 1 && row == 1), r, theta, x, y) -> X, Y, polar +if (polar) { X, Y = X*cos(Y), X*sin(Y); } +dx = (int)((X - x) * fx); dy = (int)((y - Y) * fy); +clamp col + (dx >> 5) into [0, w-1) and row + (dy >> 5) into [0, h-1) by rewriting dx, dy +negY = min(negY, dy); +cell = (dx + 0x7e0) << 12 | (dy + 0x7e0); +``` + +When the last row is done the ring height becomes `1 - (negY >> 5)`. Returns rows left. + +**GetXY (`0x0005e7d0`).** A switch on the style; out-of-range styles are rewritten to 0. Style +parameters `A0`, `A1`, `A2` are globals at `0x00188f60`, `0x00188f5c`, `0x00188f58`, rolled +only on the init cell. `rnd(n)` below is `rand() * (n / 32767)`; the binary stores each +`n / 32767` as one constant. Results are polar (radius, angle) unless noted. + +| Style | Init | X | Y | +|-------|------|---|---| +| 0 | | `(atan(0.55 - r) * 0.16 + 1) * r` | `theta - 0.01` | +| 1 | | `r * 0.87` | `theta - 0.075` | +| 2 | `A0 = (int)rnd(40) * pi` | `((sin(A0*theta) + 1) * 0.03 + 0.92) * r` | `theta` | +| 3 | `A0 = 2 + rnd(16)`, `A1 = 0.01 + rnd(0.05)`, `A2 = 0.002 + rnd(0.006)` | `max(r - A2, 0)` | `sin(A0*r) * A1 + theta` | +| 4 | | `r * 0.87` | `theta - 0.009` | +| 5 | | `r * 0.8` | `theta - 0.008` | +| 6 | `A0 = 2 + rnd(16)`, `A1 = 0.01 + rnd(0.05)` | `sin(A0*y) * A1 + x` | `sin(A0*x) * A1 + y` (x/y) | +| 7 | | `r * 1.01` | `theta + 0.021` | +| 8 | | `r - 0.01` | `theta` | +| 9 | `A0 = rnd(0.0045)`, `A1 = 0.1 + rnd(0.06)`, `A2 = 0.003 + rnd(0.005)` | `(0.99 - pow(abs(x), 1.3) * A1) * r - rnd(A0)` | `theta - A2` | +| 10 | as 9 but `A2 = 0.003 + rnd(0.006)` | as 9 with `abs(y)` | `theta + A2` | +| 11 | | `r - x * y * 0.18` | `theta - 0.005` | +| 12 | | `r * 0.96` | `theta - 0.021` | +| 13 | | `rnd(0.01) + x - 0.005` | `y - rnd(0.01) - 0.005 - (y - 1.3) * 0.04` (x/y) | +| 14 | `A0 = 0.95 + rnd(0.06)` | `A0 * r` | `theta - max(1 - r, 0) * 0.035` | +| 15 | | `r * 0.94` | `theta - 0.007` | +| 16 | | `rnd(0.008) + x * 0.9 - 0.004` | same with y (x/y) | +| 17 | | `(rnd(2) - 1) * 0.1 + x` | same with y (x/y) | +| 18 | | `r * 0.99` | `theta + 0.009` | +| 19 | | `(rnd(0.05) + 0.92) * r` | `theta + 0.003` | +| 20 | | `sin(r * 6.2831855) * 0.04 + r` | `theta + 0.015` | +| 21 | | `((sin(r * 15) + 1) * 0.05 + 0.87) * r` | `theta` | +| 22 | `A0 = rnd(0.1)`, `A1 = 3 + rnd(7)`, `A2 = 0.9 + rnd(0.3)` | `((r - A2) * 0.13 + 1) * r` | `sin(A1*r) * A0 + theta` | +| 23 | `A0 = (int)(rnd(2.1) + 1) * pi` | `((cos(A0*theta) + 1) * 0.05 + 0.89) * r` | `theta + 0.005` | + +Styles 9, 10, 13, 16, 17 and 19 call `rand()` per cell, so their fields differ every time +they're built. + +## Palettes + +### HSV2RGB (`0x0005f7e0`) + +Takes H, S, V on the stack and the output pointer in EBX. `h6 = (H - floor(H)) * 6`, sector +`= ftol(h6)`, `f = h6 - sector`. S and V are clamped to 0..1, then with `v = V * 255` +(`0x0002bf38`), `f` is replaced by `1 - f` on even sectors so one value serves as both +"falling" and "rising" channel: + +```c +m = (int)(v * (1 - S)); n = (int)(v * (1 - S * f)); v = (int)v; +switch (sector) { 1: (n,v,m) 2: (m,v,n) 3: (m,n,v) 4: (n,m,v) 5: (v,m,n) default: (v,n,m) } +``` + +The packer at `0x0005f770` clamps each channel to 0..255 and returns `0x00RRGGBB`. + +### GetHSV (`0x0005f920`) + +Recipe in EAX, outputs through EBX/EDI/ESI (H/S/V), intensity `i` and time `t` on the stack. +Stores the recipe in a global (`0x00175e70`) first; above 9 it stores 0 and runs recipe 0. +`wrap(x)` is `x - floor(x)` (`0x0005f3d0`). Exponents are doubles fed to `CIpow`. + +| Recipe | H | S | V | +|--------|---|---|---| +| 0 | `pow(i, 1.9) * 0.166` | `1` | `pow(i, 0.9)` | +| 1 | `0.45` | `pow(1 - i, 0.3)` | `pow(i, 0.7)` | +| 2 | `pow(i, 1.4) * 0.166 + 0.666` | `(pow(i * 1.8 - 1, 2) + 1) * 0.5` | `pow(i, 0.4)` | +| 3 | `wrap(t * 0.03)` | `1 - pow(i, 2.5) * 0.6` | `i` | +| 4 | `wrap(0.3 - t * 0.003)` | `pow(i, 2.9)` | `pow(i, 0.9)` | +| 5 | `wrap(t * 0.02)` | `1` | `1 - pow(i, 1.4)` | +| 6 | `pow(i, 1.6)` | `1` | `i` | +| 7 | `wrap(pow(0.7 - i * 0.4, 0.6))` | `0.9` | `pow(i, 0.9)` | +| 8 | `wrap(t * 0.002)` | `pow(1 - i, 1.5)` | `pow(i, 0.4)` | +| 9 | `wrap(t * 0.05 + (pow(cos(i * 4), 2) + pow(i, 2)) * 0.2)` | `i * 0.5 + 0.85` | `pow(i, 0.55)` | + +### Default palette (`0x0005fc30`) + +Used when a DynamicTexture has no palette node. State is global: next switch time +(`0x00188f38`), blend start (`0x00188f88`), blend recipe (`0x0017d0fc`) and blend amount +(`0x0017d0f8`); the current recipe is GetHSV's global. + +- When `now > next`: `next = rand() * (5 / 32767) + now + 10.0`, blend recipe = current, + current += 1, amount = 0, start = now. +- 256 entries, `i` stepping by `1/255` (`0x0002becc`). While blending, a second GetHSV for the + blend recipe mixes in as `(1 - amount) * old + amount * new`. That second call also rewrites + the current-recipe global, so from entry 1 on both lookups return the old recipe. +- After the loop `amount += (now - start) * 0.5`, and the blend ends at 1.0. The amount + accumulates on every call instead of tracking elapsed time. + +### Palette node + +| Offset | Field | +|--------|-------| +| 0x1c | type (recipe) | +| 0x20 | changePeriod | +| 0x24 | changePeriodRandomness | +| 0x28 | blend time, 2.0 | +| 0x2c | D3D palette | +| 0x30 | 256 `0x00RRGGBB` entries | +| 0x430 | blend recipe, -1 = none | +| 0x434 | blend amount | +| 0x438 | next change (double) | +| 0x440 | blend start (double) | + +**Advance (`0x0005d7d0`).** When `changePeriod > 0` and the time has passed: +`next = rand() * randomness / 32767 + changePeriod + now`, blend = type, type += 1, +amount = 0, start = now. Then Update, then `amount = (now - start) / blendTime`, ending the +blend at 1.0. + +**Update (`0x0005d8b0`).** Same per-entry loop as the default palette, but copies the +recipe global back into `type` after each lookup (so an out-of-range type becomes 0). Then +creates the D3D palette on first use (inlined `XMemAlloc` of the resource header plus a 0x400 +byte table) and writes every entry with alpha forced to `0xff`. + +**RenderDynamicTexture (`0x0005da80`).** Fills each row `y` with `y * 256 / height`: a ramp +through the whole palette, top to bottom. + +## DynamicTexture + +| Offset | Field | +|--------|-------| +| 0x20 | format, `0xb` (P8) | +| 0x24 | texture | +| 0x28, 0x2c | image width, height | +| 0x30..0x38 | children (count, capacity, array) | +| 0x3c | size, default 256 | +| 0x40 | erase, default 1 | +| 0x44 | fps, default 15.0 | +| 0x48 | palette node | +| 0x4c | CSurfx | +| 0x50 | last update (double) | + +**Create (`0x0005dc70`).** Always makes a 512x512, one-level texture in the node's format, +locks it, zeroes `w * h` bytes and reads the real size back. The requested size is ignored. + +**Advance (`0x0005dd20`).** CNode advance, then the palette node, then each child. + +**GetTextureSurface (`0x0005dd70`).** When `1 / fps + lastUpdate < now` (compared as floats) it +stores `now` and runs the update. Then, if the palette node's class chain reaches Palette, it +binds that node's D3D palette to stage 0. + +**Update (`0x0005dde0`).** + +1. If there's no texture and `size > 0`, Create; give up if that fails. +2. Allocate the `size x size` CSurfx on first use. +3. Only with both in place and `lastUpdate <= now`: zero the pixels when erase is set, call + every child's RenderDynamicTexture with the surface. +4. If there's no palette node, or its GetPalette returns NULL, build the default palette into a + stack buffer. The result is never read on this path. +5. Lock the texture and copy the surface in with an inlined Swizzler: per row, `IncU` per + pixel, then `IncV` and `AddU(SwizzleU(imageWidth - size))` to skip the unused columns. + +## ImageFader + +| Offset | Field | +|--------|-------| +| 0x1c | type (warp style) | +| 0x20 | changePeriod | +| 0x24 | changePeriodRandomness | +| 0x28 | current DeltaField | +| 0x2c | next DeltaField | +| 0x30 | next change time (double) | + +**RenderDynamicTexture (`0x0005e120`).** + +1. No current field: build one for `type` (new 0x3c bytes, style, Assign, SetSize to the + surface, all inlined) and set `next = changePeriod + rand() * randomness / 32767 + now`. +2. Else, when `changePeriod > 0` and `next <= now`: `type += 1`, `next = changePeriod + now`, + free the current field and promote the next one. +3. No next field: build one for `type + 1`. +4. Current field unfinished: 10 CalcSome calls, copy its style back into `type`, return without + warping. Building a field this way takes `height / 10` frames. +5. Next field unfinished: 5 CalcSome calls (guarded by a NULL check). +6. Finish the current field if needed and run the in-place fade on the surface. + +If the timer fires on the frame after `type` was set, the promoted field is NULL and step 4 +dereferences it. + +**OnSetProperty (`0x0005e3f0`).** For the `type` property (offset 0x1c): free the current field, +promote the next one, clear next. Returns 1 for everything. + +## AudioVisualizer + +| Offset | Field | +|--------|-------| +| 0x1c | type string | +| 0x20 | channel string | +| 0x24 | source node | +| 0x28 | scale, default 1.0 | +| 0x2c | offset | +| 0x30 | 256 left samples | +| 0x230 | 256 right samples | +| 0x430 | 128 left spectrum bins | +| 0x530 | 128 right spectrum bins | +| 0x630 | spectrum valid | +| 0x632 | 256 mono samples (never filled) | +| 0x832 | mono valid | + +**RenderDynamicTexture (`0x00063940`).** Returns unless the source's class is AudioClip, +its transport mode (+0x50) is 1 (playing), and its pump (+0x68) hands back a sample buffer. +Sample count is buffer size / 4, capped at 256; left/right are copied out, the rest zeroed, +both "valid" flags cleared, then the count is capped at the surface height. Channel `r`/`R` +picks the right samples. The type's first letter picks the effect: + +- anything else (or no type): **line scope.** One line per sample, from the previous point to + `(width/2 + (sample * width >> 16), row)`, color 255. +- `s`/`S`: **spinner.** Calls `rand()` and ignores it, takes `c = cos(t * 0.2)`, + `s = sin(t * 0.2)`, then steps `p` from 0 to 1 by `1 / width`: level + `= sample[(int)(count * p)] / 32767 * scale + offset`, `a = (p - 0.5) * 2.1`, point + `(c*a + s*level, s*a - c*level)` scaled by half the surface, joined with lines. +- `c`/`C`: draws nothing. +- `a`/`A`: **analyzer.** Spectrum update (below), then 256 bars: left bins reversed then right + bins, each `ln(bin) * 8` clamped to 0..255. Peaks (static ints at `0x00188f90`) fall by 2 a + frame and jump to `2 * bar`. Each column takes bar `x * 256 / width`, filled from the bottom + with 255 counting down, plus one 255 dot at the peak height (which can land in the spare + row). Then the beat flash and the wave overlay. + +**Spectrum (`0x000638a0`, `0x00063900`).** Once per frame: FFT of the left then right samples +(state made on first use), each of the 129 outputs becomes `(short)(sqrt(p) / 256)`. 129 bins +into a 128 entry array: the left's last bin lands on right bin 0 (rewritten right after) and +the right's lands on the valid flag (set right after). + +**Beat flash (`0x00063ef0`).** Bass = average of `bin >> 4` over the 5 lowest bins of both +channels. Globals at `0x0017e6f4..0x0017e70c` track a running max and min that roll into +"last" values every 10 frames (frame counter at `0x00188f50`). When bass rises above +`(6 * lastMax + 4 * lastMin) / 10` the whole surface is filled with 255, once, until bass drops +below `(4 * lastMax + 6 * lastMin) / 10`. + +**Wave overlay (`0x00064130`).** Globals: amplitude 50 (`0x00176034`), shift 30 +(`0x00176038`), mode -1 (`0x0017603c`), color 128 (`0x00176040`), next switch (`0x00188f48`). + +- When mode < 0 or the switch time passes: mode += 1, next = `rand() * 5 / 32767 + now + 3`; + mode wraps from 5 to 0. +- Half circle tables `cos/sin(i / width * pi + pi / 2)` for every 4th `i` + (`0x0017e774..0x0017e780`), rebuilt when the width changes. +- Steps of 4 pixels across the width; each step reads the high byte of sample + `((i << 8) / width) / 5` from each channel, scaled `* height * 50 >> 12`. Lines sit + `height * 30 >> 8` from the middle: mode 0 one left-channel line, mode 1 right above and left + below, mode 2 the right line twice plus two vertical left lines, modes 3 and 4 both channels + around the half circle (mode 3 clips negative values), closed with one last chord. +- Then two 64 point Lissajous trails as 2x2 dots (`0x000640a0`), color 255: + `(cos(k / d1), sin(k / d2)) * height * 50/256` rotated by `k * 0.001` around the center, with + `d1, d2 = 80, 140.48` and `187.2, 271.13`. `k` starts from a global (`0x00188f40`) each + trail and the second trail's end is stored back. + +## FFT + +A plain 256 point radix-2 decimation-in-time FFT. + +- **Tables (`0x0005f040`).** Allocates the 0x800 byte state (256 real + 256 imaginary floats), + fills a 256 entry bit-reverse table (`0x0017ccf8`) and 128 entry cos/sin tables + (`0x0017c8f8`, `0x0017caf8`) of `i * 2pi/256` (`0x0002bf40`). +- **Load (`0x0005f0f0`).** `re[i] = sample[bitrev[i]]`, `im[i] = 0`, then the passes and the + power step. +- **Passes (`0x0005f290`).** 8 passes, butterfly span 1, 2, .. 128; twiddle `j * (128 / span)`; + `t = (re*c - im*s, im*c + re*s)`, `b = a - t`, `a = a + t`. +- **Power (`0x0005f140`).** `out[i] = re^2 + im^2` for bins 0..128 (unrolled by 4), then bins 0 + and 128 times 0.25 (`0x0002beec`). + +## StarField + +`CStarField`, 0x2c bytes: stars 0x1c, speed 0x20, allocated count 0x24, star array +0x28 (x, y, z floats). The only shipping use is the music visualizer scene. -0: Firestorm, 1: Aqua, 2: Purple & Blues, 3: Color Wheel, -4: Bizarro Mystery Unveiled, 5: Bizarro Color Wheel, 6: Dark Rainbow, -7: Ice Nightshade, 8: Mystery Unveiled, 9: Roundabout +- **Factory (`0x00060390`).** stars 20, speed 1.0, no array. +- **Grow (`0x00060640`).** When there's no array or `allocated < stars`: allocate `stars` + entries, zero them, copy the old `allocated` entries over, free the old block. New stars + start at z = 0, so the next Advance scatters them. +- **Advance (`0x00060430`).** Node Advance, grow, then each star's z drops by + `speed * seconds`. A star at or past z = 0 respawns at `x, y = rand() * 2/32767 - 1` + (`0x0002bf74`) and `z = rand() * 1/32767 + 1.5` (`0x0002bf10`, `0x0002c184`). +- **RenderDynamicTexture (`0x00060510`).** Grow, then for each star with z > 0: + `inv = 1 / z`, `x = (int)((x * inv + 1) * width * 0.5)`, the same for y with height. Off + the surface, the star's z is set to 0. On it, the pixel at `y * width + x` gets + `(int)((1 - (z - 1)) * 255)` (`0x0002bf38`), so stars brighten as they come closer. -## Delta Field Styles (24 styles) +## DotField -0: Radial Breakaway, 1: Hip-no-therapy, 2: Sunburst Many, 3: Theta Divergence, -4-5: Turbo Flow Out, 6: Boxilite, 7: Collapse & Turn, 8: Constant Out, -9-10: Directrix Expand, 11: Equalateral Hyperbola, 12: Expand & Turn, -13: Gravity, 14: In/Out Inner Turn, 15: Left Turn Flow, 16: Linear Spread, -17: Noise Field, 18: Right Turn, 19: Scattered Flow, 20: Sine-Sphere, -21: Sine Multi-Circ, 22: Sphere, 23: Sunburst Few +`CDotField`, 0x50 bytes: spacing 0x1c, hSpeed 0x20, vSpeed 0x24, offset 0x28/0x2c, +velocity 0x30/0x34, target and previous spacing 0x38/0x3c, next change 0x40 and last +change 0x48 (doubles). -## Notes +- **Factory (`0x000606d0`).** spacing 8.0, speeds, offsets and velocities 0, both times + 0.0. The two spacing slots aren't touched. +- **Advance (`0x00060790`).** Node Advance. If both speeds are 0 and the next change time + has come: velocity `(rand() * 2/32767 - 1) * spacing * 4` on each axis (`0x0002c07c`), + target spacing `rand() * 24/32767 + 8` (`0x0002c180`, `0x0002bf34`), previous spacing = + spacing, last change = now, next change = `rand() * 5/32767 + now + 10`. If either speed + is set, the velocity is just (hSpeed, vSpeed). While a change is under a second old the + spacing eases linearly from previous to target; after that the last change time goes back + to 0. Last, each offset becomes `frac(offset + (velocity / spacing) * seconds)`. +- **RenderDynamicTexture (`0x00060930`).** Step is `(int)spacing`. Rows start at + `(int)(yOff * spacing)`, columns at `(int)(spacing * xOff)`, and every grid point gets 255. -- CSurfx operates on a raw BYTE array with manual pitch calculation. -- The FFT is a standard radix-2 Cooley-Tukey with precomputed twiddle factors. Buffer size is 256 samples (FFT_BUFFER_SIZE_LOG=8). -- CDynamicTexture uses the Xbox Swizzler API for texture upload. +Theseus clamps the DotField step to at least one pixel so a spacing under 1 can't loop +forever. diff --git a/docs/decomp/Util.md b/docs/decomp/Util.md index e96db2e..f6e59fc 100644 --- a/docs/decomp/Util.md +++ b/docs/decomp/Util.md @@ -34,3 +34,23 @@ Fixed-type dynamic arrays used primarily by interpolator keyframes: - **CVec4Array**: array of 4-component vectors (x, y, z, w) These provide indexed access and dynamic resizing. The interpolator nodes (CPositionInterpolator, COrientationInterpolator, etc.) store their key/keyValue data in these arrays. + +## Addresses + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0002f180 | CIntArray::SetSize | 4 byte elements; floats share the same body. Grows the buffer to exactly the new size when it is too small, copying the old values, and always stores the size | +| 0x0002f1e0 / 0x0002f2c0 / 0x0002f370 | CVec2/3/4Array::SetSize | The same for 8, 12 and 16 byte vectors (constructed through the vector constructor iterator) | +| 0x00032bb0 | CNameSpace::Define | Fails when the name is bound already; otherwise allocates `cch * 2 + 0xc` bytes (next, node, name), copies and terminates the name and pushes it on the front | +| 0x00032c20 | CNameSpace::Get | Returns the entry whose name has that exact length and matches with `wcsncmp`, or pushes a new one with no node | +| 0x00032ca0 | CNameSpace::Lookup | The node bound to the name (checks the first character, then length, then `wcsncmp`), or NULL | +| 0x00039be0 | CLerper::CLerper | Interval, start time from the frame clock (`0x0017e808`), start value read through the pointer, end value, the pointer, the owner; pushed on the list at `0x0017aeac` | +| 0x00039c20 | CLerper::AdvanceAll | For each lerper `t = (now - start) / interval`; below 1 writes `(1 - t) * start + end * t`, otherwise writes the end value, unlinks and frees it | +| 0x00039ca0 | CLerper::RemoveObject | Unlinks and frees every lerper with that owner | + +A namespace's entries are released inline where it is freed (the class destructor walks the list, +calls `Release` on each node and frees the entry). + +The out of memory path (see LoadXlate, `0x0003c370`) frees the texture cache, then the mesh cache, +and when neither frees anything prints "xdash: out of memory (failed to allocate %d bytes)" and +returns to the firmware. diff --git a/docs/decomp/VM.md b/docs/decomp/VM.md index 515e04f..cb34904 100644 --- a/docs/decomp/VM.md +++ b/docs/decomp/VM.md @@ -108,3 +108,161 @@ The tokenizer supports three comment styles: - `#` line comments - `//` line comments - `/* */` block comments + +## Runner Internals (5960 retail XBE) + +The runner code sits together at 0x36f70 to 0x39b90. Everything below is read from the retail binary. + +### CRunner Layout + +The runner is never heap allocated. `CallFunction` (0x371a0), `ExecuteScript` (0x36fb0) and `CMemberFunctionObject::Deref` (0x39810) each build one on the stack and tear it down with `ResetFunc`. + +| Offset | Field | Notes | +|--------|-------|-------| +| 0x000 | wakeup | double, sleep deadline | +| 0x008 | nop | byte offset into the op stream | +| 0x00c | ops | current op stream | +| 0x010 | sp | stack top index | +| 0x014 | stack | 100 object pointers | +| 0x1a4 | spFrame | first argument of the current call | +| 0x1a8 | spBase | top of the locals | +| 0x1ac | self | owner of the running function | +| 0x1b0 | this | object the script was started on | +| 0x1b4 | error | set by the first error, stops `Run` | +| 0x1b8 | line | last `opStatement` line | +| 0x1bc | context | saved frames, newest first | + +A saved frame (`PushContext` 0x37690, 0x1c bytes) holds the next link, ops, nop, spFrame, self, this, and the stack index under the callee and its arguments. `PopContext` (0x376e0) resolves the return value, restores the five fields (and `g_pThis`), releases every slot above the saved index, frees the frame and pushes the value. + +### Globals + +| Address | Use | +|---------|-----| +| 0x17ae9c | constructor argument count | +| 0x17aea0 | constructor argument array | +| 0x17aea4 | runner currently stepping | +| 0x17aea8 | `this` for member lookups | +| 0x17c058 | input enable flag (`EnableInput`) | +| 0x17fac8 | the `Math` object | +| 0x187fb0 | the camera | + +### Step (0x37800) + +One switch on the opcode byte. Operands follow inline: ints and floats are 4 bytes, a string is a 4 byte length followed by that many 16-bit characters, `opCall` takes a single byte. + +| Op | Operands | Behavior | +|----|----------|----------| +| opNull | | push null | +| opNew | int n, string class | `CreateNewObject` with the n values on the stack, release them, push the result. Unknown class: `Failed to create new object: "%s"` (name cut to 255) | +| opVar | string | push a new unresolved name object | +| opJump | uint | jump | +| opCond | uint | pop, resolve, `ToNum`; nonzero skips the operand, zero or null jumps | +| opCall | byte n | see below | +| opRet | | with a saved frame, `PopContext`; at the top, pop the result into the out pointer and stop | +| opStr / opNum | string / float | push a string / number | +| opAdd .. opXor | | `BinaryOperator` | +| opDot / opArray | | pop key and base, both must be non-null; `opArray` resolves the key fully; the base is resolved one step and asked `Dot(key)`. A null answer reports `Unknown member: %s` but still pushes | +| opAssign | | pop value (resolved) and target; null target is `cannot assign to null`; else `target->Assign(value)`, push value | +| opLocal | int i | slot `spFrame + i`; creates a `CLocalVariable` there on first use, pushes it with a new reference | +| opThis | | push self | +| opFrame | int n | `sp += n`, `spBase = sp`; no size check | +| opStatement | int line | store the line | +| opNeg | | pop and resolve; not a number is `expected a number`; pushes `0.0 - v` | +| opDrop | | pop and release | +| opEndFrame | int n | release n slots off the top, `spBase = sp` | +| opSleep | | pop and resolve; null is `illegal null reference`, not a number `expected a number`; wakeup = now + v | + +Any other byte stops the runner. + +`opCall` looks at the object under the n arguments: + +- type 8 (native function ref): call it, release it and the arguments, push the result. +- type 14 (bound method): AddRef it, `PushContext(n)`, `spFrame = sp - n`, self = its owner, ops = its code. +- otherwise it is a name. `ExecuteBuiltIn` gets the first try; if it handled the call the argument slots are cleared without a release, the name is released and the result pushed. Then `LookupFunction`; a hit pushes a frame and sets self and `g_pThis` to the owner. A miss is `unknown function: %s` with the name cut to 31 characters. + +There is no null check on the callee. + +### BinaryOperator (0x37fe0) + +Pops right then left, each resolved fully. + +1. Either null, or both type 5 (node): `==` and `!=` compare the pointers, anything else is `illegal null reference`. +2. Both strings and the op is not `+`: `wcscmp` (null text reads as empty), then `== != < > <= >=` push 1 or 0. Anything else is `type mismatch`. +3. Both numbers: `+ - * /`, the six comparisons, and `& | ^ << >>`. The bit ops convert both sides with `_ftol`, mask the shift count to 5 bits, and turn the result back into a float as unsigned (a negative int gets 4294967296.0 added, the constant at 0x2bed0). Any other op, including `%`, falls out of the switch and pushes the right operand unchanged. +4. Otherwise `+` concatenates the two `ToStr` forms and anything else is `type mismatch error`. + +The compiler never emits `%`, so the fallthrough is unreachable from script. + +### Errors (0x375e0) + +Only the first error of a run counts. The message is formatted into 512 characters, wrapped with or without the line number, and the error flag is set. Retail never displays it; the run just stops. + +### Built-ins (0x38620) + +Matched by length, then by text: `EnableInput` (1 arg, else `Invalid parameter`), `eval` (1 arg, else `Bad launch`), `launch` (1 or 2 args, else `Bad launch`), `DebugBreak` (does nothing), `alert` (1 arg, debug print, else `Bad alert`), `log` (1 arg, does nothing with it in retail, else `Bad log!`). + +`launch` with one argument copies the path, turns its last `/` into `\`, then cuts at the last `\` to get the directory. There is no check that a `\` was found. Both paths go through `CleanFilePath`, a lock at 0x17aeb0 is taken exclusive, and the title is launched through `XWriteTitleInfoAndRebootA` with launch data type -1. + +### Lookup + +`LookupMember` (0x399e0) sets `g_pThis`, then tries in order: a node property (type 5 objects only, wrapped in a `CProperty` whose ref count is set to 0), the object's own members, each class up the base chain, and then the same lookup on each parent. + +`LookupFunction` (0x389b0) tries `FindMemberFunction` on self, then `LookupMember` on each parent. A hit on a parent makes that parent the owner. A hit that is not a function (type 13) is returned as a raw pointer anyway. + +`CVarObject::Deref` (0x394e0) special-cases the names `Math` and `camera`, then looks the name up on self and, if self is not `this`, on `this`. A miss is `Unknown object: %s`; `Assign` (0x395b0) reports `Unknown variable: %s`. + +### Value Objects + +| Type | Class | Vtable | Notes | +|------|-------|--------|-------| +| 2 | CNumObject | 0x22c50 | `ToStr` (0x38fd0) prints `%f` into 20 characters, then trims trailing zeros and a trailing point | +| 4 | CVarObject | 0x22cf8 | owns a copy of the name | +| 7 | CNodeArrayObject | 0x22d30 | `length`, `add`, `remove` become function refs 0, 1, 2; a number indexes, out of range is `Array reference out of bounds` | +| 8 | CFunctionObject | 0x22cc0 | index plus a referenced owner | +| 9 | CMember | 0x22dd8 | variable slot on whatever `g_pThis` is | +| 10 | CMemberVarObject | 0x22da0 | variable slot on a referenced instance | +| 13 | CFunction | 0x22c18 | code size at +0x14, ops from +0x18 | +| 14 | CMemberFunctionObject | 0x22d68 | reading one without calling runs it on a fresh runner | +| 16 | CVec3Object | 0x22c88 | node class `Vector3`; `ToStr` is `%f %f %f` | +| | CLocalVariable | 0x22e48 | one reference to its value; `Deref` swaps the slot reference for a value reference | +| | CProperty | 0x22e10 | node plus PRD | + +`CProperty::Deref` (0x38b00) wraps integer, boolean and number fields as numbers, vec3 as a `Vector3`, node fields as the node itself, strings as a new string, and children / node array fields as a `CNodeArrayObject` over the field. Anything else is `Unknown property type`. + +`CProperty::Assign` (0x38d70) refuses null for everything but node fields (`Illegal null assignement`, spelled that way in the binary). Integers truncate, booleans test against 0.0, a vec3 field only takes a `Vector3`, and other types are ignored. + +## Array (0x30440..0x30e70) + +`CArrayObject` is the script's `Array`: object type 15, 0x20 bytes, length at 0x14, +allocated slots at 0x18, element pointers at 0x1c. Its function table at `0x0017f5d8` +holds `concat` (sig 17), `join` (5), `reverse` (15), `slice` (0, raw parameter list) and +`sort` (15). + +- **Script constructor (`0x00030630`).** With one argument the argument is dereferenced + until it stops changing, written back into the parameter slot, converted with `ToNum` + and truncated: `new Array(5)` is five nulls. With two or more, the length is the + argument count and each argument is stored with an AddRef. No arguments, no block. +- **Native constructor (`0x00030770`).** `n` zeroed slots, or none for 0. +- **Destructor (`0x00030800`).** Releases each non-null element, frees the block. +- **Grow (`0x000308e0`).** Only grows. When the new length passes the allocation it + allocates `n + 16` slots, copies the old length over and frees the old block. The new + tail is zeroed. +- **Dot (`0x00030970`).** A variable named `length` gives a new number; a number gives a + `CArrayElementReference` for `(int)n`; anything else goes to the base Dot. +- **Element reference.** Holds an AddRef on the array (`0x00030440`, released at + `0x000304c0`). `Assign` (`0x00030520`) grows to `index + 1` when `length <= index`, + releases the old slot and stores the new object with an AddRef. `Deref` (`0x00030570`) + reports `Array reference out of bounds` for a negative or too large index; otherwise it + AddRefs the element, releases the reference itself and returns the element. +- **concat (`0x00030a70`).** Errors with `object is not an array` unless the argument is + type 15; otherwise a new array of both arrays' elements. +- **join (`0x00030b80`).** Builds a new string: each element's `ToStr` text (an empty + string when it has none), `[null]` for an empty slot, and the separator between + elements. `ToStr` (`0x00030960`) is `join(",")`. +- **reverse (`0x00030c40`)** and **sort (`0x00030e70`)** return new arrays. Sort is the + CRT `qsort` with the comparator at `0x00030e00`: two nulls are equal, a null sorts + first, otherwise `wcscmp` of the two `ToStr` texts. +- **slice (`0x00030cd0`).** Takes one or two parameters, else `invalid number of + parameters`. Start is `(int)` of the first; end is the second or -1. A negative end + becomes `end + length + 1`, so the default means "through the last element". The count + is `end - start`, floored at 0. Start is never range checked. diff --git a/docs/decomp/XIP.md b/docs/decomp/XIP.md index d019b69..861ecce 100644 --- a/docs/decomp/XIP.md +++ b/docs/decomp/XIP.md @@ -1,81 +1,213 @@ -# XIP +# XIP Archives + +What we found in the Xbox Dashboard about its XIP archives: one `XIP0` file per scene, holding +its scripts, textures, raw data and meshes. Everything here comes from the 5960 retail XBE. +Theseus rebuilds it in `render/xip_archive.cpp`, one file for both builds. + +## Functions + +| Address | Name | What it does | +|---------|------|--------------| +| 0x0003d390 | (index buffer alloc) | Builds an index buffer resource by hand, with the out of memory retry | +| 0x0003d410 | (vertex buffer alloc) | Same for a vertex buffer | +| 0x0003d4a0 | CFileBuffer::SetFile | Keeps the handle, finds the archive in the signed `XIPS` section | +| 0x0003d570 | (block hash check) | Hashes a 64 KB block and compares it with the signed table | +| 0x0003d5e0 | CFileBuffer::Seek | Loads the block holding an offset | +| 0x0003d710 | CFileBuffer::Read | Copies out of the block buffer, refilling it as it goes | +| 0x0003d900 | LoadXIP | Finds or opens an archive in the pool and starts its load | +| 0x0003dab0 | FindInXIPAndDetach | Hands a raw entry's bytes to the caller | +| 0x0003dba0 | FindObjectInXIP | Looks an entry up across every loaded archive | +| 0x0003dd20 | CXipFile::~CXipFile | Frees tables, mesh buffers and the block buffer | +| 0x0003ddf0 | CXipFile::Open | Stores the file name and the lookup key | +| 0x0003dec0 | CXipFile::StartLoadThread | Thread entry: load, or reload the mesh buffers | +| 0x0003def0 | CXipFile::Load | Reads the header, the tables and every entry | +| 0x0003e030 | (texture read) | XPR0 entry to a texture | +| 0x0003e110 | CXipFile::CreateObjects | Reads the entries in file order | +| 0x0003e270 | (name compare) | `bsearch` compare, `_stricmp` | +| 0x0003e290 | CXipFile::Find | Path to entry index | +| 0x0003e320 | CXipFile::FindObject | Path to object, with a type check and an `AddRef` for textures | +| 0x0003e390 | CXipFile::DeleteMeshBuffers | Releases all ten buffer pairs | +| 0x0003e440 | CleanupMeshCache | Frees the mesh buffers of the archive drawn longest ago | +| 0x0003e5f0 | CXipFile::ReadIndexBuffer | Index buffer entry to a buffer | +| 0x0003e650 | CXipFile::ReadVertexBuffer | Vertex buffer entry to a buffer | +| 0x0003e710 | CXipFile::ReloadMeshBuffers | Reads every mesh buffer again after an eviction | +| 0x0003e800 | CXipFile::Reload | Flags a reload and starts the loader (labeled `CMeshRef_Render` in the project) | +| 0x0003e850 | CMeshRef::Render | CMeshRef vtable (`0x00024dbc`) slot 0 | +| 0x0003ead0 | CMeshRef::GetFVF | Slot 1 | + +Globals: the pool `c_rgXipFile` at `0x001804e0` (twenty 0x150 byte slots, torn down by the +`atexit` thunk at `0x000ce600`), the count at `0x0017e734`, the frame clock the cache times come +from at `0x0017e808`, the XIP folder string at `0x0017e828`, the out of memory stage at +`0x0017c028`, and the disk lock every `ReadFile` holds shared at `0x0017aeb0`. + +## The file + +| Offset | Size | Field | +|--------|------|-------| +| 0 | 4 | magic `XIP0` (`0x30504958`) | +| 4 | 4 | data start | +| 8 | 2 | entry count | +| 0xa | 2 | name count | +| 0xc | 4 | data size | + +Then one 16 byte record per entry (data offset, size, type, timestamp), one 4 byte directory record +per name (entry index, name offset), the name pool, and the data. `Load` sizes the name pool as +`dataStart - 16 - entries * 16 - names * 4` and never checks it. + +Entry types, from the `CreateObjects` switch: + +| Type | What `CreateObjects` makes | +|------|----------------------------| +| 1 | nothing, and nothing is read | +| 2 | a texture (`0x0003e030`) | +| 4 | a 0x14 byte CMeshRef: archive, `offset >> 24` as the buffer, `offset & 0xffffff` as the first index, `size` as the primitive count | +| 5 | an index buffer, in the slot of the vertex buffer count at that moment | +| 6 | a vertex buffer: a 4 byte vertex count and a 4 byte FVF lead the vertices | +| anything else | `size` raw bytes from `XAppAllocMemory` | + +Entries are read back to back with no seeking, so the data has to be in table order. The +directory is sorted, because `Find` binary searches it. + +## Reading the file + +`Load` opens the archive with `NO_BUFFERING | SEQUENTIAL_SCAN` (`0x28000080`), after turning a +leading `y:` into `x:`. Unbuffered handles only read whole aligned blocks, so every read goes +through `CFileBuffer`, one 64 KB `VirtualAlloc` block at a time. The object is six dwords: the +handle, the signed section, the buffer, the bytes the last block gave, the read index, and one past +the block in the buffer. -XIP ("XIP0") is the archive format used by the Xbox Dashboard to bundle scene scripts (.xap), textures (.xbx), meshes (.xm), and other assets into a single file. The dashboard loads XIPs at startup and on skin changes, caching mesh data in GPU-accessible memory. Backed by `xip_archive.cpp` and `xip_archive.h` in the active source tree. +``` +Read(pv, cb): // 0x0003d710 + if (!buffer) buffer = VirtualAlloc(0x10000); // retry: texture cache, mesh cache, then reboot + for (;;) { + if (cb < 1) return total; + if (index < size) { copy min(size - index, cb); advance; } + if (index != size) continue; + lock shared 0x17aeb0; ok = ReadFile(h, buffer, 0x10000, &got); unlock; + if (!ok) HalReturnToFirmware(4); + if (got == 0) return -1; + hash check the block; current++; size = got; index = 0; + } +``` -## File Format +The buffer refills the moment it runs dry, even when the copy just finished, so a read ending on +the file's last byte returns -1. `Seek` reads the block an offset falls in unless `current` already +names it; since `current` is one past the block in the buffer, seeking back into that block reads +it again. -Derived from hex analysis of .xip files. +Retail archives are signed. `SetFile` walks the `XIPS` section for the archive's file name and +keeps its record, and every block `Read` or `Seek` loads is run through `XCCalcDigest` and +compared with the record's next 20 byte hash (`0x0003d570`). A missing record or a wrong hash +calls `HalReturnToFirmware(4)`, the fatal error reboot. -### Header (16 bytes) +## The pool -``` -Offset Size Field -0x00 4 Magic ("XIP0" = 0x30504958) -0x04 4 DataStart (byte offset where file data begins) -0x08 2 FileCount (number of FILEDATA entries) -0x0A 2 NameCount (number of FILENAME entries) -0x0C 4 DataSize (total size of all file data) -``` +`LoadXIP` puts relative names under `Q:/Xips/`, cleans the path, drops the extension, and compares +that with every slot's key. A hit stamps the slot's cache time and, if its mesh buffers were +evicted, reads them back before returning it. Otherwise it opens the next slot, counts it, and +loads it on a new thread; if no thread starts, it runs the loader inline. There is no bound check: +a twenty first archive would write past the pool. Slots are never freed. Nothing in the binary +unloads an archive; only its mesh buffers come and go. -### FILEDATA entries (16 bytes each) +`Open` keeps the path without its extension as the key, except that an archive named `default` +gives up its own name too, so `Q:\Xips\default.xip` answers for `Q:\Xips`. `LoadXIP`'s own +comparison keeps `default`, so a second `LoadXIP` of the default archive would open a new slot. -``` -Offset Size Field -0x00 4 DataOffset (from DataStart, or packed mesh ID for type 4) -0x04 4 Size (byte count, or primitive count for type 4) -0x08 4 Type (0=generic, 1=mesh[obsolete], 2=texture, 3=wave, 4=meshref, 5=IB, 6=VB) -0x0C 4 Timestamp -``` - -### FILENAME entries (4 bytes each) +`FindObjectInXIP` and `FindInXIPAndDetach` search newest archive first and skip any that is +locked or not loaded. For a texture asked of slot 0, `FindObjectInXIP` swaps the file name's +extension for `xbx` and looks it up under the XIP folder instead, since the default archive packs +its textures as `.xbx`. `FindInXIPAndDetach` hands the raw bytes and size to the caller and clears +the archive's pointer, so the caller frees them. ``` -Offset Size Field -0x00 2 FileDataIndex (index into FILEDATA array) -0x02 2 NameOffset (offset into name string pool) +Find(url): // 0x0003e290 + n = strlen(dirPath); + if (_strnicmp(url, dirPath, n) || url[n] != '\\') return -1; + hit = bsearch({names, url + n + 1}, directory, nameCount, 4, compare); + return hit ? hit->entry : -1; + +FindObject(url, type): // 0x0003e320 + n = Find(url); + if (n == -1 || (type != -1 && filedata[n].type != type)) return 0; + if (type == 2) objects[n]->AddRef(); + return objects[n]; ``` -### Layout +## Textures ``` -[XIPHEADER][FILEDATA * FileCount][FILENAME * NameCount][name strings][file data...] +texture(file, cb): // 0x0003e030 + xpr = new(cb); file.Read(xpr, cb); + if (xpr->magic != 'XPR0') return 0; + tex = AllocNoncontiguous(0x14); copy 0x14 bytes from xpr + 0xc; + bits = AllocContiguous(xpr->total - xpr->headerSize, 0x80); + if (!bits) return 0; + copy the pixels from xpr + headerSize; + tex->Data = 0; Register(bits); tex->Common |= 0x01000000; + return tex; ``` -Directory is sorted by name for binary search (bsearch with _stricmp). - -## Binary Analysis (4920-5960 retail XBE) - -### Globals +`Register` is inlined: the address is masked to physical unless the resource is a push buffer. +The entry's bytes are never freed, and a failed pixel allocation leaks the header. -- `c_nXipFileCount` at `0x0001c028`: count of loaded archives -- `c_rgXipFile[20]`: static pool of CXipFile instances +## Mesh buffers -### CMeshRef vtable +A CXipFile carries ten 24 byte buffer pairs at `+0x2c` (FVF, stride, vertex count, index count, +vertex buffer, index buffer), then the vertex buffer count `+0x11c`, the index buffer count +`+0x120`, the cache time `+0x128`, the lock flag `+0x130`, the `CFileBuffer` `+0x134` and the +reload flag `+0x14c`. The loaded flag is `+0x28`. -- `___7CMeshRef__6B_` at `0x000249fc` +The buffers are built without the device: `0x0003d390` and `0x0003d410` `XMemAlloc` a resource +header and the data, and the readers write straight into it. `ReadVertexBuffer` first waits for +the GPU if the buffer is still in use. The stride is `(size - 8) / count`, with no check on the +count. `ReloadMeshBuffers` seeks to each buffer entry in table order and reads it again, numbering +index buffers and vertex buffers separately. -### File Protection - -Retail binary calls `HalReturnToFirmware(HalFatalErrorRebootRoutine)` on XIP read failure. Debug builds issue an ALERT and `int 3` breakpoint. - -### Texture Loading (XPR format) - -Textures inside XIPs are wrapped in XPR containers (Xbox Packed Resource). The loader checks the XPR magic, allocates GPU-contiguous memory via `TheseusD3D_AllocContiguousMemory`, copies texture data, and registers it with D3D. - -### Mesh Buffer Layout - -Vertex buffers are prefixed with an 8-byte header: `[int vertexCount][DWORD fvf]`, followed by raw vertex data. Index buffers are raw WORD arrays. - -Mesh references pack the buffer index and first-index into a single DWORD: +Under memory pressure `XAppAllocMemory` and the block allocator call `CleanupTextureCache` first, +then `CleanupMeshCache`, which frees the buffers of the unlocked, loaded archive with the oldest +cache time that still has some. The next `CMeshRef::Render` of that archive sees a NULL vertex +buffer and calls `Reload`; `CInline_Render` (`0x0004e5d0`) checks the same thing inline +(`loaded && !reloading && vbCount && !meshBuffers[0].vb`) and reloads before drawing. ``` -DataOffset = (bufferIndex << 24) | firstIndex +CMeshRef::Render(bSetFVF): // 0x0003e850 + xip->cacheTime = now; buf = &xip->meshBuffers[index]; + if (xip->reloading) return; + if (!buf->vb) { xip->Reload(); return; } + if (bSetFVF) SetVertexShader(GetFixedFunctionShader(buf->fvf)); + if (count > 800 && !g_bEdgeAntialiasOverride) { EDGEANTIALIAS = 0; MULTISAMPLEANTIALIAS = 1; } + if (g_uMeshRef++ == 0) { + view = inverse(affine(rotation 0x00187fe0, position 0x00187fd4)), third row negated; + wv = matrix stack top * view; + c0..3 = transpose(wv * projection); c10..13 = transpose(wv); + c5..8 = inverse(wv); c4 = the light direction in object space; + } + SetStreamSource(0, buf->vb, buf->stride); SetIndices(buf->ib, 0); + DrawIndexedVertices(TRIANGLELIST, count * 3, indices + first); ``` -### Skin Override - -`FindObjectInXIP` checks the skin directory for loose texture files before searching loaded archives. This allows skins to override individual textures without repacking the XIP. - -### Cache Eviction - -`CleanupMeshCache` finds the oldest loaded XIP (by `m_cacheTime`) that has mesh buffers allocated and frees them. Mesh buffers are lazily reloaded on next render via `CMeshRef::Render`. +The constant numbers above are the binary's minus its bias of 96. Theseus sets the same constants +on every world matrix change instead (see `SetFalloffShaderFrameValues`, `0x0005ca00`). + +## Where Theseus differs + +- No `XIPS` signature check and no `y:` to `x:` swap; files open through Theseus's own path layer. +- The pool grows twenty slots at a time instead of overrunning at twenty one, so a long desktop + session never runs out. Archives still never leave it. A sync `LoadXIP` that fails returns NULL. +- `Load` checks every read, refuses a header whose name pool would be empty or whose + `dataStart + dataSize` isn't the file size, and closes a refused file. +- `CreateObjects` refuses mesh references past the last slot and mesh buffers past ten, and reads + past what it refuses so the entries after stay in line. Textures must hold an XPR header and a + resource header, and their sizes must fall inside the entry. +- `Load` also refuses tables that don't hold together: a name outside the pool or an unterminated + pool, a directory record naming an entry that doesn't exist, an entry whose bytes run past the + data, or a vertex buffer too short for its count and FVF. +- `Seek` refuses an offset past the end of the file, which would leave `Read` spinning, and reload + skips a buffer it can't seek to. +- Reload refuses buffers `CreateObjects` refused. `CMeshRef` refuses an index past the buffers and + a slice that runs past its index buffer. +- `FindObject` skips the `AddRef` on a texture that failed to load, `FindInXIPAndDetach` hands out + raw entries only, and `LoadXIP` refuses a path with no extension or one too long to build. +- Buffers are made through the device (bgfx on desktop) and locked to write. +- Desktop: loads and reloads run inline, textures are decoded in software, raw entries get a + terminating zero, a refused read is refused instead of rebooting, and there is no edge AA swap. diff --git a/docs/desktop/README.md b/docs/desktop/README.md index 3855a9b..07c6cf6 100644 --- a/docs/desktop/README.md +++ b/docs/desktop/README.md @@ -1,12 +1,12 @@ -# Desktop Port Documentation +# Desktop Documentation -Technical documentation for the desktop port of Theseus: the Xbox dashboard engine running natively on macOS, Linux, and Windows via SDL2 + OpenGL. +How the Xbox dashboard engine runs on macOS, Linux and Windows as UIX Desktop: SDL2 for windows, input and sound, bgfx for rendering (Metal, Direct3D 11, Vulkan), libmpv for video. -For the engine itself (Script VM, scene graph, XIP archives), see [`docs/decomp/`](../decomp/). For the XAP script API contract that both Xbox and desktop builds must honor, see [`docs/xap-contract.md`](../xap-contract.md). +For the engine itself (script VM, scene graph, XIP archives) see [`docs/decomp/`](../decomp/). For the script API both builds have to honor, see [`docs/xap-contract.md`](../xap-contract.md). -## Contents - -- **[Porting to PC](porting-to-pc.md)**: How Xbox D3D8, kernel APIs, and filesystem calls were replaced with SDL2/OpenGL for macOS, Linux, and Windows -- **[D3D8 to OpenGL Translation](d3d8-translation.md)**: The rendering layer that makes Xbox Direct3D 8 calls work on modern OpenGL 3.2 -- **[Desktop Features](desktop-features.md)**: Game launching, Steam integration, Title Maker, development tools, media player -- **[Media Player Design](media-player-design.md)**: libmpv integration with the Xbox DVD player XAP scene +- **[Building](building.md)**: every desktop target, the one time bgfx and libdatachannel steps, Windows cross builds, packaging +- **[Dependencies](dependencies.md)**: packages per OS, minimum library versions, the Windows cross deps +- **[Desktop Features](desktop-features.md)**: the games grid and launching, Title Maker, media, music, xCloud, skins, CRT, the tool windows, settings files +- **[Porting to PC](porting-to-pc.md)**: the platform layer, drive letters and the user data folder, audio, input, nodes, logs +- **[D3D8 to bgfx](d3d8-translation.md)**: how the dashboard's Direct3D 8 calls are drawn through bgfx +- **[Media Player](media-player-design.md)**: the media library, libmpv playback, and what became of the DVD player diff --git a/docs/desktop/building.md b/docs/desktop/building.md new file mode 100644 index 0000000..2423e3c --- /dev/null +++ b/docs/desktop/building.md @@ -0,0 +1,135 @@ +# Building UIX Desktop + +Everything builds from one Makefile in `build/`. The libraries each OS needs, and the versions they have to clear, are in [dependencies.md](dependencies.md). The CI workflows in `.github/workflows/` build every desktop target on every push, so when this page and the workflows disagree, trust the workflows. + +| Target | Command (from `build/`) | Output | +|---|---|---| +| macOS / Linux, native | `make desktop` | `~/builds/theseus/desktop/theseus` | +| Windows x64, cross from macOS or Linux | `make desktop-win64` | `~/builds/theseus/desktop-win64/theseus.exe` | +| Linux ARM64, cross from x86_64 Debian/Ubuntu | `make desktop-linux-arm64` | `~/builds/theseus/desktop-linux-arm64/theseus` | + +`BUILDS_ROOT=/some/dir` moves the output somewhere other than `~/builds/theseus`. `DEBUG=1` builds with `-g -O0`, and `ASAN=1` adds AddressSanitizer to a native build. + +## 1. Clone with submodules + +bgfx, bx, bimg, projectM and libdatachannel are submodules under `theseus/third-party/`. + +``` +git clone --recursive https://github.com/MrMilenko/Theseus +# or, in an existing clone: +git submodule update --init --recursive +``` + +## 2. Install the packages + +See [dependencies.md](dependencies.md) for the per OS package lists. The build finds every library through `pkg-config`, so the package manager doesn't matter. If something sits off the default search path, point `PKG_CONFIG_PATH` at its `.pc` files. On macOS with Homebrew, curl is keg only: + +``` +export PKG_CONFIG_PATH="$(brew --prefix curl)/lib/pkgconfig:$PKG_CONFIG_PATH" +``` + +## 3. Build the bgfx runtime libraries (once per platform) + +Only `bx`, `bimg` and `bgfx` are needed. bgfx's own top level targets also build `shaderc`, `geometryc` and a debug config, which takes far longer for nothing this build uses. + +``` +cd theseus/third-party/bgfx + +# macOS (Apple Silicon) +../bx/tools/bin/darwin/genie --gcc=osx-arm64 gmake +make -C .build/projects/gmake-osx-arm64 -j$(sysctl -n hw.ncpu) bx bimg bgfx config=release + +# Linux +../bx/tools/bin/linux/genie --gcc=linux-gcc gmake +make -C .build/projects/gmake-linux-gcc -j$(nproc) bx bimg bgfx config=release64 + +# Windows, cross from Linux +../bx/tools/bin/linux/genie --gcc=mingw-gcc gmake +make -C .build/projects/gmake-mingw-gcc -j$(nproc) bx bimg bgfx config=release64 \ + CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ +``` + +Always give `-j` a number. A bare `-j` starts every compile at once, and on a small machine that runs it out of memory and can freeze the whole desktop. If a low RAM machine with no swap still struggles, use a smaller number than the core count. The same goes for every `make` and `cmake --build` below. + +The `genie` that ships prebuilt in bx needs a recent glibc. On an older distro it fails with `GLIBC_2.38 not found`; build it from source and carry on: + +``` +git clone --depth 1 https://github.com/bkaradzic/GENie.git /tmp/genie +make -C /tmp/genie +cp /tmp/genie/bin/linux/genie theseus/third-party/bx/tools/bin/linux/genie +``` + +The compiled shaders are checked in under `Data/shaders/` for every backend (Metal, SPIR-V, Direct3D 11, GLSL, ESSL), so a normal build never runs `shaderc`. Only if you edit a `.sc` file in `theseus/desktop/shaders/` do you need the `shaders-bgfx*` targets, and those want a built `shaderc`. + +## 4. Build libdatachannel (once per platform) + +This is the WebRTC transport behind Xbox Cloud Gaming and remote play. It's easy to skip and the failure is quiet: the Makefile only defines `THESEUS_HAVE_WEBRTC` when it finds the static library, so without this step everything compiles and links and xCloud is simply missing. The folder name matters: `build-mac`, `build-linux` or `build-win64`, because that's where the Makefile looks. + +``` +cd theseus/third-party/libdatachannel + +# macOS +cmake -S . -B build-mac -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" +cmake --build build-mac -j$(sysctl -n hw.ncpu) + +# Linux +cmake -S . -B build-linux -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 +cmake --build build-linux -j$(nproc) +``` + +The Windows cross build of it is in [Windows](#windows) below. + +## 5. Build and run + +``` +cd build +make desktop +~/builds/theseus/desktop/theseus +``` + +The first `make desktop` also builds the vendored projectM into a local prefix and drops its shared library in `lib/` beside the binary. Each build copies `Configs/`, `Data/` and `Library/` next to the binary and stamps `Configs/version` with the build date and commit. + +When the app runs, the copies beside the binary are only the starting point. Your settings and library live in the per user data folder (see the main README), seeded from those copies on first run. + +## Windows + +The release is cross compiled from Linux with mingw-w64 (`x86_64-w64-mingw32-g++`, posix thread model). The pieces mingw doesn't ship: + +- **SDL2 and SDL2_mixer** mingw devel tarballs from libsdl.org, unpacked into `~/cross/sdl2-mingw`. The preamble of the `desktop-win64` section in `build/Makefile` has the exact commands. +- **libmpv** dev archive (shinchiro's mpv-winbuild-cmake releases) into `~/cross/mpv-mingw`, and the **curl** mingw zip from curl.se into `~/cross/curl-mingw`. +- **OpenSSL, opus and a trimmed FFmpeg**: `build/fetch-mingw-deps.sh` puts them in `~/cross/mingw64`. [dependencies.md](dependencies.md) explains why FFmpeg is built rather than fetched. +- **libdatachannel**, cross built against that prefix: + +``` +cmake -S theseus/third-party/libdatachannel \ + -B theseus/third-party/libdatachannel/build-win64 \ + -DCMAKE_TOOLCHAIN_FILE="$PWD/build/cmake-toolchains/mingw-w64-x86_64.cmake" \ + -DCMAKE_FIND_ROOT_PATH="$HOME/cross/mingw64" \ + -DOPENSSL_ROOT_DIR="$HOME/cross/mingw64" \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \ + -DNO_WEBSOCKET=1 -DNO_EXAMPLES=1 -DNO_TESTS=1 \ + -DENABLE_WARNINGS_AS_ERRORS=OFF +cmake --build theseus/third-party/libdatachannel/build-win64 -j$(nproc) +``` + +Then `make desktop-win64`. The output folder is a complete drop: the exe, every runtime DLL, and Configs/Data/Library. The `release-win64` job in `.github/workflows/release.yml` is the whole recipe end to end. + +`make desktop` under an MSYS2 MinGW64 shell is also detected, but the cross build is the one CI tests. + +## Linux ARM64 + +The release ARM64 build runs natively on an ARM64 runner with the same steps as x64, plus a GENie built from source. To cross compile from an x86_64 Debian or Ubuntu host instead, enable the arm64 multiarch packages listed at the top of `build/Makefile` and run `make desktop-linux-arm64`. The binary links against the build host's glibc, so build on a host whose glibc is no newer than the target's. + +## Packaging + +- **macOS app**: `build/mk-macapp.sh` wraps a desktop build into `~/builds/theseus/Theseus.app`. It copies the Homebrew dylibs into `Contents/Frameworks`, rewrites their install names to `@rpath` so the bundle runs without your Homebrew layout, builds the icon, and ad hoc signs the result. Ad hoc is enough to run it; handing it to other people properly needs a Developer ID and notarization. +- **Linux AppImage**: `build/mk-appimage.sh [build dir] [output]` bundles the binary with every library it pulls in, except the ones that have to come from the host (glibc, GPU drivers, X11, Wayland, audio servers), and needs `patchelf`. + +## Before you send a change + +- `make lint-platform`: only `theseus/desktop/host_*` may include OS headers or test OS macros. `_WIN32` is true on the Xbox too, so shared code goes through `host.h` instead. +- `make win64-import-guard`: compiles the Windows objects and fails if anything outside `host_win*` calls the Win32 file, clock or sleep functions directly. +- Build every desktop target you can, and the Xbox build if you touched `engine/`, `shared/` or `render/`. diff --git a/docs/desktop/d3d8-translation.md b/docs/desktop/d3d8-translation.md index 484a74b..4ea650c 100644 --- a/docs/desktop/d3d8-translation.md +++ b/docs/desktop/d3d8-translation.md @@ -1,184 +1,80 @@ -# D3D8 to OpenGL Translation +# D3D8 to bgfx -## The Problem +## The problem -The Xbox dashboard renders everything through Direct3D 8, Microsoft's graphics API from 2000. D3D8 predates programmable shaders (it has vertex shaders but no pixel shaders in the modern sense), uses a fixed-function texture blending pipeline, and has a completely different state model from modern OpenGL. +The Xbox dashboard draws everything through the Xbox flavor of Direct3D 8, on an NV2A GPU with a fixed function texture combiner and a handful of custom vertex shaders. The scene graph code calls `SetRenderState`, `SetTextureStageState`, `SetTransform` and `DrawIndexedPrimitive` like any 2001 game, and it expects Xbox texture formats and Xbox vertex layouts. -The desktop port doesn't use a general-purpose D3D8-to-OpenGL translator. Instead, `d3d8_sdl.h` implements just enough of the D3D8 interface to render the dashboard, approximately 2,600 lines of purpose-built translation code. +The desktop build doesn't carry a general D3D8 emulator. `theseus/desktop/d3d8_sdl.h` implements the slice of the D3D8 interface the dashboard actually uses and backs it with [bgfx](https://github.com/bkaradzic/bgfx), which picks Metal on macOS, Direct3D 11 on Windows and Vulkan on Linux (overridable in Settings > Display > Renderer). The dashboard code above it doesn't know the difference. -## Architecture +## Shape of the shim -The translation layer provides stub classes that mirror the D3D8 COM interfaces: - -``` -D3D8 Interface Desktop Implementation -───────────────── ────────────────────── -IDirect3D8 → Returns a single "SDL OpenGL" adapter -IDirect3DDevice8 → OpenGL state machine + GLSL shaders -IDirect3DTexture8 → GL texture objects -IDirect3DVertexBuffer8 → CPU-side vertex arrays with transform caching -IDirect3DIndexBuffer8 → CPU-side index arrays -IDirect3DSurface8 → GL framebuffer attachments -``` - -## IDirect3DDevice8: The Core - -Every D3D8 call in the dashboard goes through `CDashApp` wrapper functions that call into `IDirect3DDevice8`. On desktop, this class manages: - -### Render State Tracking - -D3D8 uses `SetRenderState()` with ~200 possible state flags. The translation layer tracks the ones the dashboard actually uses: - -| D3D8 Render State | OpenGL Equivalent | -|-------------------|-------------------| -| `D3DRS_ZENABLE` | `glEnable/glDisable(GL_DEPTH_TEST)` | -| `D3DRS_ALPHABLENDENABLE` | `glEnable/glDisable(GL_BLEND)` | -| `D3DRS_SRCBLEND` / `D3DRS_DESTBLEND` | `glBlendFunc()` | -| `D3DRS_CULLMODE` | `glEnable(GL_CULL_FACE)` + `glCullFace()` | -| `D3DRS_LIGHTING` | Tracked as a flag, applied in vertex shader | -| `D3DRS_FILLMODE` | `glPolygonMode()` | -| `D3DRS_ALPHATESTENABLE` / `D3DRS_ALPHAREF` | Fragment shader `discard` | -| `D3DRS_TEXTUREFACTOR` | Uniform passed to fragment shader | - -The dashboard sets ~40 different render states during initialization. Most are Xbox-specific display modes (flicker filter, soft display filter, swath width) or features not needed on desktop (stencil, fog, point sprites). These are accepted by `SetRenderState` without error but have no GPU-side effect. - -### Texture Stage States - -D3D8 uses a multi-stage texture blending model. Each stage can have a texture bound and configure how its color and alpha are combined with previous stages: - -```cpp -// Dashboard typically does: -SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); -SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); -SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); -SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); -``` - -The translation layer tracks stage 0 and stage 1 configurations and generates GLSL fragment shader logic to match. The most common patterns: -- **MODULATE**: `texture_color * vertex_color` (the default) -- **SELECTARG1**: Use texture color only -- **SELECTARG2**: Use vertex color only -- **ADD**: `texture_color + vertex_color` - -### Transform Pipeline - -D3D8 uses separate World, View, and Projection matrices set via `SetTransform()`. The translation layer captures these and combines them into an MVP (Model-View-Projection) matrix for the GLSL vertex shader: - -```cpp -// D3D8 code sets: -SetTransform(D3DTS_WORLD, &matWorld); -SetTransform(D3DTS_VIEW, &matView); -SetTransform(D3DTS_PROJECTION, &matProjection); - -// Translation layer combines to: -mat4 MVP = Projection * View * World; -// Passed to vertex shader as uniform ``` - -## GLSL Shaders - -The dashboard's rendering is handled by two GLSL shaders: - -### Vertex Shader - -```glsl -// Simplified. Actual shader handles more cases. -uniform mat4 u_MVP; -uniform vec4 u_falloffCenter; -uniform float u_falloffRadius; -uniform float u_falloffIntensity; - -in vec3 a_position; -in vec4 a_color; -in vec2 a_texcoord; - -out vec4 v_color; -out vec2 v_texcoord; - -void main() { - gl_Position = u_MVP * vec4(a_position, 1.0); - v_color = a_color; - v_texcoord = a_texcoord; -} +D3D8 interface Desktop implementation +IDirect3D8 One adapter, one device +IDirect3DDevice8 Render state mirror, packed into bgfx state and shader uniforms +IDirect3DTexture8 CPU side RGBA pixels, uploaded to a bgfx texture on first use +IDirect3DVertexBuffer8 Vertex data plus a bgfx layout built from the FVF on first draw +IDirect3DIndexBuffer8 WORD indices, expanded to triangle lists at draw time +D3DX math Matrices, vectors, quaternions, inline ``` -### Fragment Shader - -```glsl -// Simplified. Actual shader handles TSS state combinations. -uniform sampler2D u_texture; -uniform bool u_hasTexture; -uniform float u_alphaRef; - -in vec4 v_color; -in vec2 v_texcoord; - -out vec4 fragColor; +Everything the dashboard draws in a frame lands in bgfx view 0. The CRT effect, video and the ImGui tools layer on after. -void main() { - vec4 texColor = u_hasTexture ? texture(u_texture, v_texcoord) : vec4(1.0); - fragColor = texColor * v_color; - if (fragColor.a < u_alphaRef) discard; -} -``` - -The actual shaders are more complex, handling independent color and alpha operations, multiple texture stages, and various blend modes configured by the TSS tracking. +## Render state -## Per-Vertex Falloff Lighting +D3D8 has a couple hundred render states. The shim tracks the ones the dashboard sets and folds them into a `BGFX_STATE_*` bitmask that rides along with each draw: -The dashboard's signature visual effect is a radial falloff, a spotlight-like effect where objects near the camera's focus are bright and objects further away fade to dark. On Xbox, this was implemented as a custom vertex shader (`effect.vsh`) running on the NV2A GPU. +| D3D8 | bgfx | +|---|---| +| `D3DRS_ZENABLE`, `D3DRS_ZFUNC`, `D3DRS_ZWRITEENABLE` | depth test bits and `BGFX_STATE_WRITE_Z` | +| `D3DRS_ALPHABLENDENABLE`, `D3DRS_SRCBLEND`, `D3DRS_DESTBLEND` | blend function bits | +| `D3DRS_CULLMODE` | cull bits (bgfx's default winding is clockwise, same as D3D) | +| `D3DRS_ALPHATESTENABLE`, `D3DRS_ALPHAREF` | a shader uniform, `discard` in the fragment shader | +| `D3DRS_TEXTUREFACTOR` | a shader uniform | -On desktop, this is computed on the CPU during vertex transformation: +The rest are accepted and dropped: NV2A display tricks (flicker filter, soft display, swath width), fog, stencil, point sprites, vertex blending. The dashboard sets them during init and never relies on them. -``` -For each vertex: - 1. Transform position by World-View-Projection matrix - 2. Compute distance from vertex to falloff center (in view space) - 3. Calculate attenuation: brightness = 1.0 - saturate(distance / radius) - 4. Multiply vertex color by attenuation - 5. Store result in vertex diffuse color -``` +Sampler state (`D3DTSS_ADDRESSU/V`, `MINFILTER`, `MAGFILTER`) becomes `BGFX_SAMPLER_*` flags passed with the texture on each draw. -The falloff parameters (center, radius, intensity) are extracted from the D3D8 vertex shader constants that the dashboard sets. Multiple shader variants exist for different numbers of lights and different vertex formats. +F8 dumps a count of how often each render and texture stage state is set, and how many of those sets changed nothing. Shift+F8 resets the counters. -## Matrix Caching +## The fixed function emulator -The device caches the combined World-View-Projection (`m_matWVP`) and World-View (`m_matWV`) matrices, recomputing them only when a dirty flag is set by `SetTransform()`. This avoids redundant matrix multiplications when the same camera/world state is used across multiple draw calls in a frame. +Two bgfx shaders stand in for the whole fixed function pipeline: `vs_ff.sc` and `fs_ff.sc` in `theseus/desktop/shaders/`. Rather than compiling a shader per state combination, the shim packs the current state into a few `vec4` flag uniforms and the shaders branch on them. -## Texture Handling +**Vertex side** (`vs_ff`): -### XBX Textures (Xbox native) +- Transforms by the world view projection matrix, or passes pretransformed `XYZRHW` vertices straight through for 2D overlays +- Picks the vertex color from falloff, vertex diffuse, texture factor, material diffuse or plain white +- Reads Xbox `NORMPACKED3` normals (11:11:10 in four bytes) as well as float normals +- Does spherical environment mapping for the materials that use it -Xbox textures use DXT compression with swizzled pixel layout: +**Falloff** is the dashboard's signature look: the faint glow along the edges of the green shell and the brighter faces pointing at you. On the Xbox it's `effect.vsh`. The desktop shader does the same math on the GPU: side color plus (front color minus side color) times the absolute dot product of the view direction and the normal. Faces turned toward the camera get the front color and edges fade to the side color. Both colors come from the material, through the same vertex shader constants the Xbox code sets. -1. **Read XBX header**: Extract width, height, format (DXT1/DXT3/DXT5), mipmap count -2. **Deswizzle**: Convert from Morton order to linear scanline order -3. **Upload to OpenGL**: `glCompressedTexImage2D()` with the appropriate GL format: - - DXT1 → `GL_COMPRESSED_RGBA_S3TC_DXT1_EXT` - - DXT3 → `GL_COMPRESSED_RGBA_S3TC_DXT3_EXT` - - DXT5 → `GL_COMPRESSED_RGBA_S3TC_DXT5_EXT` +**Fragment side** (`fs_ff`) covers the texture stage setups the dashboard uses: modulate, select texture, select diffuse and add for color; modulate or select for alpha; an optional second stage that adds color or multiplies alpha; and the alpha test. With no texture bound, a 1x1 white texture keeps the sample well defined. -### Standard Textures (Desktop) +## Draw calls -PNG, JPG, and BMP textures (used by skins and the Title Maker) are loaded via `stb_image` and uploaded as uncompressed RGBA. +- `DrawIndexedPrimitive` submits through a dynamic vertex buffer and a transient index buffer. +- `DrawPrimitiveUP` and `DrawIndexedPrimitiveUP` copy the caller's data into transient buffers each draw. +- Strips, fans and the Xbox only `D3DPT_QUADLIST` are expanded to triangle lists, so bgfx only ever sees triangles. The scratch buffer for that lives for the whole run. +- Vertex layouts come from the FVF on first use. Float normals map to bgfx's normal attribute; packed normals ride in a color attribute and get unpacked in the shader. -### Texture Cache +## Textures -The dashboard loads textures frequently as scenes change. A texture cache (`TXTCACHE[100]`) stores recently-used GL textures by name. On skin changes, the cache is flushed and all `CImageTexture` nodes are marked dirty for reload. +Dashboard textures are `.xbx` files: Xbox XPR0 resources holding DXT1, DXT3, DXT5 or linear data, usually swizzled into Morton (Z) order for the NV2A. `xbx_texture.h` parses the header, decodes DXT blocks on the CPU and unswizzles to plain RGBA. PNG, JPG and BMP (skins, Steam icons, box art) go through `stb_image`. Either way the result is RGBA pixels on the CPU side of `IDirect3DTexture8`. -## What's NOT Translated +Upload to bgfx happens the first time a texture is drawn. Textures the dashboard rewrites every frame (the music visualizer, dynamic textures) are created with `BGFX_TEXTURE_BLIT_DST`. Without it, Metal can put the texture in GPU only memory and silently ignore later CPU updates, and the visualizer freezes on its first frame. -The translation layer deliberately skips D3D8 features the dashboard doesn't use: +## Things drawn outside the scene -- **Pixel shaders**: D3D8 pixel shaders aren't used by the dashboard (it uses TSS instead) -- **Stencil buffer**: Not used -- **Multi-render-targets**: Not used -- **Fog**: Set during init but not rendered (states accepted, no GL calls) -- **Point sprites**: Not used -- **Vertex declarations** (beyond FVF): The dashboard uses FVF exclusively +A few things skip the D3D8 shim entirely: -This keeps the translation layer small and focused. A general-purpose D3D8 wrapper would be 10x larger; this is purpose-built for one application. +- **CRT effect**: when it's on, view 0 renders into an offscreen framebuffer and `fs_crt.sc` draws that to the backbuffer with scanlines, curvature, phosphor mask, vignette, bloom, flicker and color bleed. +- **Video**: libmpv renders each frame into a CPU buffer that's uploaded to a bgfx texture and drawn fullscreen with `vs_blit` and `fs_blit` (see [media-player-design.md](media-player-design.md)). It goes through the CRT pass too. +- **Boot animation and xCloud streams**: the same blit path. +- **ImGui tools**: their own bgfx backend (`imgui_impl_bgfx.cpp`) with `vs_imgui` and `fs_imgui`. -## Coordinate System Differences +## Shaders on disk -D3D8 uses a left-handed coordinate system (Y up, Z into screen). OpenGL uses right-handed (Y up, Z out of screen). The translation layer handles this by negating the Z component in the projection matrix, which flips the winding order. `glFrontFace(GL_CW)` compensates for the reversed triangle winding. +Shader sources are `theseus/desktop/shaders/*.sc`. The compiled binaries for every backend are checked in under `Data/shaders/` (`metal`, `spirv`, `dx11`, `glsl`, `essl`), and the app loads the folder matching the renderer bgfx picked. They're the one part of `Data/` that stays with the app instead of moving to the user data folder. If that folder is missing, the log says so near the top, because otherwise the only symptom is a black window. diff --git a/docs/desktop/dependencies.md b/docs/desktop/dependencies.md index 11834b6..4a6e233 100644 --- a/docs/desktop/dependencies.md +++ b/docs/desktop/dependencies.md @@ -1,7 +1,81 @@ # Desktop dependencies What the desktop build links against on each platform, the floor each library -has to clear, and where the Windows cross-deps come from. +has to clear, and where the Windows cross deps come from. The build steps +themselves are in [building.md](building.md). + +## Packages + +Everything is found through `pkg-config`, and nothing in the build shells out +to a package manager. If `pkg-config --exists mpv libcurl libavcodec opus` +succeeds, the build will find them, however they got there. Prefixes off the +default search path just need `PKG_CONFIG_PATH`. + +"Run" is what a downloaded release needs. "Build" is that plus the toolchain +and headers. The Windows zip and the Linux AppImage carry their own libraries, +so they need nothing installed. + +**macOS (Homebrew)** + +``` +# Run the bare binary release (the .app needs nothing) +brew install sdl2 sdl2_mixer mpv curl ffmpeg opus + +# Build +brew install sdl2 sdl2_mixer mpv curl ffmpeg opus pkg-config cmake openssl@3 +``` + +**macOS (MacPorts)** + +``` +# Run +sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus + +# Build +sudo port install libsdl2 libsdl2_mixer mpv curl ffmpeg libopus \ + pkgconfig cmake openssl3 +``` + +**Debian / Ubuntu** + +``` +# Run the tarball release +sudo apt install libsdl2-2.0-0 libsdl2-mixer-2.0-0 libmpv2 libcurl4 libopus0 + +# Build +sudo apt install build-essential pkg-config cmake \ + libsdl2-dev libsdl2-mixer-dev \ + libvulkan-dev libx11-dev libgl-dev \ + libmpv-dev libcurl4-openssl-dev libssl-dev \ + libavcodec-dev libavutil-dev libswscale-dev libopus-dev +``` + +**Fedora** + +``` +sudo dnf install gcc-c++ pkgconf-pkg-config cmake SDL2-devel SDL2_mixer-devel \ + mpv-libs-devel libcurl-devel openssl-devel \ + ffmpeg-devel opus-devel vulkan-loader-devel libX11-devel +``` + +**Arch** + +``` +sudo pacman -S base-devel pkgconf cmake sdl2 sdl2_mixer mpv curl openssl \ + ffmpeg opus vulkan-icd-loader libx11 +``` + +Notes: + +- Some distros ship `libmpv1` instead of `libmpv2`. Either works. +- The FFmpeg runtime libraries come in with libmpv. FFmpeg and Opus also + cover the xCloud video and audio decoders. +- OpenSSL is a runtime dependency of xCloud. On macOS it arrives alongside + curl, mpv and FFmpeg, and on Debian `libssl3` is in the base install, so + neither run list names it. Building libdatachannel does need the headers, + which is why `openssl@3` and `libssl-dev` are in the build lists. +- projectM is built from the submodule on the first `make desktop` and ships + in `lib/` beside the binary, so it isn't a package you install. ## Minimum versions @@ -36,11 +110,13 @@ should link and run. | glibc | n/a | 2.41 | n/a | The Linux binary links libavcodec 61 and glibc 2.41, and a tester on -libavcodec 62 could not load a build made against 61. That is why the Linux -tarball bundles its whole `ldd` closure except the host-tied libraries -(glibc, GPU driver GL/EGL/Vulkan/DRM, X11/xcb/Wayland/xkb). +libavcodec 62 could not load a build made against 61. That is why the +AppImage bundles the binary's whole `ldd` closure except the libraries that +have to come from the host (glibc, GPU driver GL/EGL/Vulkan/DRM, X11, xcb, +Wayland, audio servers). The tarball only carries projectM in `lib/` and +uses the system's copies of everything else. -## Windows cross-deps +## Windows cross deps `build/fetch-mingw-deps.sh` populates `~/cross/mingw64` and both CI workflows call it. Two different strategies in there: @@ -54,10 +130,10 @@ call it. Two different strategies in there: HEVC is enabled even though xCloud sends H.264. `h2645_sei.o` calls `ff_aom_uninit_film_grain_params`, and libavcodec only compiles -`aom_film_grain.o` when an HEVC config is on, so an h264-only build leaves that +`aom_film_grain.o` when an HEVC config is on, so an H.264 only build leaves that symbol dangling. -The other cross-deps (SDL2, SDL2_mixer, libmpv, libcurl) are fetched by the +The other cross deps (SDL2, SDL2_mixer, libmpv, libcurl) are fetched by the workflows into `~/cross/{sdl2-mingw,mpv-mingw,curl-mingw}`. ### Two traps worth knowing @@ -72,7 +148,7 @@ against the static archive. Its libsrtp also needs `-DENABLE_WARNINGS_AS_ERRORS=OFF`, because `u_long` is 32 bit under LLP64 and its `%x` format checks trip `-Werror=format`. -## Streaming is opt-in at build time +## Streaming is optional at build time xCloud and Xbox remote play only compile in when libdatachannel is present: `build-mac`, `build-linux`, or `build-win64` under diff --git a/docs/desktop/desktop-features.md b/docs/desktop/desktop-features.md index d8209ed..d943cde 100644 --- a/docs/desktop/desktop-features.md +++ b/docs/desktop/desktop-features.md @@ -1,196 +1,108 @@ # Desktop Features -## Beyond a Port +UIX Desktop is the Xbox dashboard first and a PC launcher second. The scene scripts drive everything you see; the desktop build adds what a PC needs underneath them, plus a set of ImGui tool windows for managing your library and building skins and scenes. This page is the tour. Setup steps for users are in the main [README](../../README.md). -UIX Desktop isn't just the Xbox dashboard running on PC. It adds desktop-native features that turn it into a usable game launcher and development tool. The dashboard's scripting system handles the UI, while the platform layer adds PC-specific functionality underneath. +## The games grid -## Game Launching +On the Xbox, the dashboard found games by walking `E:\Games` and reading each `default.xbe` for its title and icon. The desktop build keeps that exact flow and feeds it entries that mostly aren't folders at all: -Games are added and managed through the **Title Maker** (F3). You don't need to create files or edit configs by hand: the Title Maker handles everything: game entries, icons, launch commands, and categories. +- **`Configs/games.ini`**: everything added in Title Maker, including Steam and RetroArch imports. One section per entry with `TitleID`, `Launch`, `Drive` and `Category`. +- **ES-DE**: read live from your ES-DE gamelists every launch, under one ES-DE category with a folder per system. +- **Xbox Cloud Gaming**: your cloud library and consoles, fetched after sign in and injected for the session. +- **Real folders** under `Library/Games`, `Library/Applications` and `Library/Emulators`, the same as on an Xbox. -### Adding Games - -- **Manually**: Enter a name, launch command, and optional icon -- **Steam import**: One-click scan of your Steam library: discovers installed games, downloads icons from Steam CDN, and creates entries with `steam://` launch URIs -- **Xbox ISOs**: Browse for an ISO file: the Title Maker extracts the game title and cover art automatically, then creates an xemu launch entry -- **xemu setup**: A "Find" button checks common install paths on each platform (`/Applications/xemu.app`, `/usr/bin/xemu`, `C:\Program Files\xemu\`, etc.) - -Games appear on the dashboard organized by category (Games, Applications, Homebrew, Emulators, etc.) and drive letter, just like the Xbox. - -### How It Works (Under the Hood) - -The Title Maker writes to `games.ini`, which the virtual filesystem layer reads at runtime. When the dashboard scans for games (via `FindFirstFile`), the platform layer injects virtual directory entries from `games.ini` alongside any real folders in `Library/Games/`. The dashboard doesn't know the difference: it sees what looks like real Xbox game folders. - -Icons are stored in `Configs/icons/` and mapped via `Icons.ini`. The dashboard's icon selector searches both UDATA title images and the desktop icon database. +The file layer makes the first three list like folders, and their `default.xbe` opens as a synthesized header carrying the name and title ID. Icons come from `Configs/icons/.jpg` or `.png`. For each Title Maker entry the desktop also writes a small `Library/UDATA//TitleMeta.xbx`, so the Memory screen shows a title pod per game the way the Xbox did. ### Launching -When you select a game, the dashboard: -1. Hides the SDL window -2. Spawns the game process (`fork`/`exec` on POSIX, `CreateProcess` on Windows) -3. Waits for the process to exit -4. Restarts itself via `execv()` (POSIX) or relaunches via `CreateProcess` (Windows) with `--preview` to skip the preloader - -Launch commands can be direct executables, Steam URIs (`steam://rungameid/...`), xemu with an ISO path, or any shell command. The dispatch logic lives in `theseus/desktop/launch.cpp` and follows three rules: +`theseus/desktop/launch.cpp` takes the launch line from the entry: -- **`steam://...`**: invokes the `steam` binary directly on Linux to bypass KIO. KDE/Plasma's URL chain often has no registered handler for the `steam` scheme; calling `steam` directly avoids the "Unknown protocol" error path entirely. macOS and Windows still go through their native URL handlers, which work fine there. -- **Other URLs** (`http://`, `https://`, `file://`, ...): system URL opener (`xdg-open` / `open` / `ShellExecute`). -- **Anything else**: runs through the shell (`/bin/sh -c` on POSIX, `cmd /C` on Windows). Raw executables, paths with arguments, bash scripts, multi-token commands like `firefox --new-window URL` all work. Quote any path containing spaces. +- `xcloud://` and `xhome://` start a cloud or remote play stream inside the dashboard window. +- `steam://` links go to Steam. On Linux the `steam` binary is called directly, because some desktop environments have no handler registered for the scheme. +- Other URLs go to the system URL opener. +- Anything else runs as a shell command from the program's own folder, so executables, scripts, commands with arguments and `$VARIABLES` all work. Quote paths with spaces. -The same dispatcher is used by Title Maker's "Test Launch" button (fire-and-forget, no window hide) and by the dashboard's `.uixshortcut` / virtual-game launches (full hide + wait + restart pipeline). +A real launch fades the screen to black the way the Xbox's launch level did, spawns the game, and minimizes the dashboard so the game gets focus. Title Maker's Test Launch uses the same path without the fade or the minimize, and shows how the spawn went. The full command line is always in the log. ## Title Maker (F3) -The Title Maker is the central tool for managing your game library. It supports adding games manually, importing from Steam, and adding Xbox ISOs with automatic metadata extraction. Each entry gets a name, launch command, icon, category, and drive letter. Icons can be PNG, JPG, or BMP: Steam imports download icons automatically from Steam CDN. - -## Save Game Browser - -The dashboard's Memory section is fully functional on desktop. It reads real Xbox save data from `Library/UDATA/` (or from a mounted qcow2 FATX image) and renders the same save game browser as the original Xbox, complete with: - -- **Title pods** with game icons loaded from `TitleImage.xbx` -- **Title headers** with game names parsed from `TitleMeta.xbx` (UTF-16LE with localization support) -- **Save icon rows** with per-save images (falls back to title-level `SaveImage.xbx` when a save doesn't have its own icon) -- **Save metadata** (name, date, block count) exposed via node functions for the XAP script to display -- **Smooth scrolling** between titles with animated transitions -- **D-pad navigation** to browse titles and individual saves - -The rendering is adapted from the Xbox source's custom `RenderLoop`: it positions pods, headers, and icon rows using D3D transformation matrices rather than the normal scene graph. This matches the Xbox's approach where `SavedGameGrid` was a separate rendering subsystem. - -To test with your own saves, copy Xbox `UDATA` folders into `Library/UDATA/`. Each title folder should contain `TitleMeta.xbx` (the game name) and optionally `TitleImage.xbx` (the icon), with save subfolders containing `SaveMeta.xbx`. - -## Development Tools - -Development tools are gated behind **Development Mode** (set via startup chooser or Settings > General > Startup Mode). In Dashboard Mode, only user-facing features (Title Maker, HDD Browser) appear in the Tools menu. In Development Mode, the Inspector and XAP Editor become available. - -### XAP Script Editor (F2) - -A live script editor for dashboard development, built on ImGuiColorTextEdit: - -- **Syntax highlighting** for VRML/XAP keywords, node types, strings, numbers, and comments -- **Undo/redo** (Ctrl+Z / Ctrl+Y) with full edit history -- **Live reload**: Edit a script, save, and the dashboard reloads the scene without restarting -- **Scene browser**: Navigate inline scenes and browse .xap files on disk -- **Error reporting**: Compiler errors show line numbers and descriptions -- **Floating ImGui panel**: Independent from all other tools, resizable and repositionable - -### Scene Inspector (F1) - -A read-only scene graph browser for understanding what the dashboard is doing: - -- **Scene tree**: Expandable hierarchy of all nodes with search/filter -- **Visibility toggles**: Click to hide/show any node or subtree -- **3D click-to-select**: Click any object in the viewport to select it in the tree (pulsing green highlight) -- **Right-click context menu**: Solo/unsolo, show/hide, recursive operations on subtrees -- **Node properties** (read-only): Transform (position, scale, rotation, alpha), material (diffuse, emissive, transparency), texture (name, dimensions, thumbnail preview), geometry info, mesh file -- **Script variables**: All CInstance members shown with types and current values -- **Stats bar**: Draw call count, visible/total nodes, group count, wireframe toggle - -Property editing is currently read-only. A future update will wire edits back to the XAP source for round-trip editing. - -### Xbox HDD Browser (F5) - -Browse and manage Xbox hard drive images: - -- **qcow2 support**: Mount Xbox HDD images (the format xemu uses) -- **FATX filesystem**: Full read-write access to the Xbox file system -- **Title enumeration**: Browse installed games, saves, and system files -- **Integration**: SavedGameGrid reads titles and icons from mounted images - -## CRT Post-Processing - -An optional retro display filter accessible from Settings (F4) or the View menu, implemented as a full-screen GLSL fragment shader with 8 independent parameters: +The central tool for the library. Tabs: -- **Scanlines**: Horizontal darkened lines simulating a CRT display -- **Curvature**: Barrel distortion simulating a curved CRT screen -- **Phosphor mask**: RGB sub-pixel pattern -- **Vignette**: Corner and edge darkening -- **Bloom**: Glow/light bleed from bright areas -- **Flicker**: Brightness variation simulating unstable signal -- **Color bleed**: Horizontal color smear -- **Brightness**: Overall intensity adjustment +- **Main**: every entry, with search and a category filter. Edit the name, launch line, icon and category; Test Launch; Delete. Create entries by hand, add Xbox ISOs as xemu entries (the title and art are read from the ISO), and Fix Names to tidy up imported titles. +- **Steam**: finds the Steam install (or browse to it), imports every installed game with icons from Steam, adds betas and demos by App ID, opens store pages. +- **RetroArch**: finds the install, imports recently played titles with the right core and box art from RetroArch's thumbnails, or adds a ROM with a core picked from the list. Launch fullscreen is a toggle. +- **ES-DE**: point it at the ES-DE data folder. Rescan rereads it; Compress Art re-encodes everything in `Configs/icons` to 256 pixel JPEGs, which keeps big collections light. -**Presets** (Off, Subtle, Classic, Heavy) set all 8 parameters at once, or each can be tuned individually in the Settings window. Settings persist in `desktop.ini`. CRT effects also apply to media playback. +Steam, RetroArch and ES-DE can each be hidden under Optional Tabs. Hiding one only hides the tab. -## Audio +## Media -The desktop build uses SDL_mixer for audio playback: +- **Local library**: Movies and TV folders, with TMDB posters and synopses if you add a key. +- **Plex**: sign in with a PIN, browse and stream your server. +- **Jellyfin**: server URL plus Quick Connect. +- **Playlist Maker (F6)**: build playlists from the library. -- **Dashboard sounds**: Button clicks, transitions, navigation feedback -- **Background music**: Soundtrack playback from the music directory -- **Mute toggle**: Ctrl+M to mute/unmute all audio (persistent toast overlay) -- **Auto-mute during game launch**: Audio pauses when a game is running -- **IMA ADPCM decoding**: Xbox audio format decoded in software +How the player is wired is in [media-player-design.md](media-player-design.md). -## Media Player +## Music -The dashboard's original Xbox DVD player UI (`dvd.xap`) has been wired to libmpv for file-based media playback. When a media file is opened (File > Open Media), the DVD player scene loads and renders video through the XAP's `texture_panel` mesh while the OSD overlay shows playback info. +The dashboard's music screen reads from your music folder (Settings > Media > Local Library > Music Root): every subfolder is a soundtrack, `.m3u` files become soundtracks, loose files collect in one called Music. Formats: MP3, WAV, OGG, FLAC, WMA, M4A, AAC. Tags and durations are read once and cached. -See [Media Player Design](media-player-design.md) for the full technical breakdown. +With **Use projectM visualizer** on (Settings > Audio), X + Y on the keyboard or a pad opens [projectM](https://github.com/projectM-visualizer/projectm), the open source MilkDrop, in its own window, fed from whatever the dashboard is playing. F or F11 makes it fullscreen. Configure projectM picks presets. -## Preloader +## Xbox Cloud Gaming and remote play -When the application starts (without a startup-mode CLI flag), a startup screen lets you choose: +Sign in under Settings > Media > Xbox / xCloud with a device code. Your Game Pass cloud titles and your consoles appear in the games grid; launching one streams it inside the dashboard window over WebRTC (libdatachannel), with FFmpeg decoding the video and Opus the audio. The same panel lists your consoles with a Stream button for each. -- **Dashboard Mode**: Uses compiled `.xip` binary archives (production mode, user-facing features only) -- **Development Mode**: Uses extracted script directories (supports live editing, enables Inspector and XAP Editor) +## Skins -The choice is saved to `desktop.ini` and can be changed later in Settings > General > Startup Mode. +A skin is a folder in `Data/Skins/`: a `.xbx` file listing material colors, plus any textures and meshes it replaces. The Stock skin is the retail 5960 dashboard, and its colors live in the source (`theseus/render/materials_retail.inl`), so `Stock.xbx` lists nothing. Other skins only list the materials they change. -## CLI Flags +The **Skin Editor** (Tools > Skin Editor) lists your skins, applies one live, creates a new one from Stock, and edits material colors with a picker before saving them to the skin. UIX Lite community skins work as they are. -Pass these on the command line when launching the binary: +## CRT effect -| Flag | What it does | -|------------------|-------------------------------------------------------------------------------| -| `--scale N` | UI scale factor for the ImGui debug overlay (`1.5`, `2.0` for high-DPI / 4K) | -| `--fullscreen` | Borderless fullscreen at native resolution | -| `--4k` | Convenience: `--fullscreen` + `--scale 2.0` | -| `--no-toolbar` | Start with the menu bar hidden (toggle back with F10) | -| `--dashboard` | Start in dashboard mode (skip the preloader picker) | -| `--development` | Start in development mode (enables Inspector, XAP Editor) | -| `--muted` | Start with audio muted | -| `--preview` | Legacy alias for `--dashboard` | +A post process pass over the finished frame, video included, with eight controls: scanlines, curvature, phosphor mask, vignette, bloom, flicker, color bleed and brightness. Subtle, Classic, Heavy and Off presets set them all at once. It's in Settings > Display and View > CRT Effect, and saved in `desktop.ini`. -`--scale` writes to `ImGui::GetIO().FontGlobalScale`. ImGui's `imgui.ini` only stores window layout (positions, sizes, docking state); UI scale is not persisted there and must be re-supplied each launch. +## Development tools -## Menu Bar Visibility +- **Scene inspector (F1)**: the live scene graph as a tree with search. Click in the viewport to select what's under the cursor, hide or solo nodes and subtrees, and read transforms, materials, textures (with a thumbnail), meshes and script variables. A stats bar shows draw calls and node counts, and there's a wireframe toggle. +- **XAP editor (F2)**: a script editor with XAP syntax highlighting, undo and redo, and compile errors with line numbers. Save and the scene reloads in place. +- **Xbox HDD browser (F5)**: opens an xemu qcow2 hard drive image read only, lists its FATX partitions, browses them and exports files. Set the image in Settings > General and the Memory screen reads saves from it too. -The top menu bar can be toggled three ways: +**Development Mode** (chosen at launch when Startup Mode is Ask on launch, or set in Settings > General) unpacks the shipped `.xip` archives into folders under `Data/Xips/` in your user data folder and runs the scenes from those loose files, so script edits show up without repacking. The Development menu only appears in that mode. -- **F10**: global hotkey, toggles in and out -- **File menu, Hide Menu Bar**: discoverable menu item (gone with the bar; F10 brings it back) -- **`--no-toolbar`**: CLI flag, starts hidden +## Display and window -Useful when running fullscreen and you want a clean viewport. +Settings > Display covers display mode (windowed, borderless, fullscreen), resolution, vsync, FPS cap, MSAA, renderer, hardware video decode and Emulate Xbox Resolution, which keeps scenes framed in the original 720x480 design space. F11 toggles fullscreen, F12 a borderless window, F10 the menu bar (it can also auto hide). -## Configuration +### Command line -Desktop-specific settings are stored in `Configs/desktop.ini`: +| Flag | What it does | +|---|---| +| `--fullscreen` | Start fullscreen | +| `--4k` | Fullscreen plus 2x UI scale | +| `--scale N` | Scale the tool windows (for example `1.5` or `2.0` on a high DPI screen) | +| `--no-toolbar` | Start with the menu bar hidden | +| `--muted` | Start muted | +| `--minimized` | Start minimized | +| `--dashboard` | Skip the startup choice and run the dashboard | +| `--development` | Start in Development Mode | +| `--graphics-debug` | Extra renderer diagnostics | -```ini -XemuPath=/Applications/xemu.app -QcowPath=/path/to/xbox.qcow2 -StartupMode=1 -CRT_Enabled=0 -CRT_Scanlines=0.3 -CRT_Curvature=0.2 -CRT_Phosphor=0.3 -CRT_Vignette=0.3 -CRT_Bloom=0.15 -CRT_Flicker=0.2 -CRT_ColorBleed=1.0 -CRT_Brightness=1.05 -``` +## Controllers -Dashboard settings (skins, video mode, language, etc.) use the same `config.ini` as the Xbox version. The configuration system is shared between platforms. +SDL2's GameController API covers Xbox, PlayStation, Switch Pro and most other pads. The dashboard reads them exactly like the Xbox's four ports: D-pad and stick to move, A to select, B to back out, the triggers and Black and White where the scenes use them. LT + B hands the pad to the tool windows, with a prompt bar showing what each button does and an on screen keyboard for text fields. -## Controller Support +## Settings files -SDL2's GameController API provides native support for: +All in `Configs/` inside the user data folder: -- Xbox controllers (wired and wireless via Bluetooth/USB) -- PlayStation DualShock 4 and DualSense controllers -- Nintendo Switch Pro Controller -- Any SDL2-compatible gamepad +| File | What it holds | +|---|---| +| `desktop.ini` | Desktop settings: tool paths (xemu, Steam, RetroArch, ES-DE, qcow2 image), display and CRT, audio, library folders, and Plex, Jellyfin and Xbox sign in tokens | +| `config.ini` | The dashboard's own settings, the same file the Xbox build uses (current skin, main menu wiring, clock and so on) | +| `games.ini`, `Icons.ini`, `icons/` | The launcher library | +| `playlists.ini` | Playlists from the Playlist Maker | -The dashboard maps controller input to the same navigation model as the Xbox: D-pad for movement, A for select, B for back, triggers for page scrolling. All XAP-visible callbacks, the secret key sequence system, screen saver reset, and typomatic repeat logic are preserved. +Every save writes a new file, flushes it to disk and swaps it in, keeping the previous one as `.old`. diff --git a/docs/desktop/media-player-design.md b/docs/desktop/media-player-design.md index 9f9d622..202358d 100644 --- a/docs/desktop/media-player-design.md +++ b/docs/desktop/media-player-design.md @@ -1,119 +1,58 @@ -# Media Player (libmpv + DVD XAP) +# Media Player -The dashboard's original Xbox DVD player UI (`dvd.xap`) is wired to libmpv for file-based media playback on desktop. Video renders through the XAP scene's `texture_panel` mesh while the OSD overlay shows playback info using the same script that ran on the Xbox. +The Xbox could play DVDs, provided you bought the little IR remote dongle that unlocked it. On desktop there's no disc to spin, so UIX Desktop plays your files and your Plex and Jellyfin libraries instead, through libmpv, behind scenes styled like the rest of the dashboard. -## How Xbox DVD Playback Worked - -### XAP Script Flow - -1. `theDiscDrive` (DiscDrive node) has property `discType` (string) -2. When `discType` changes, XAP callbacks fire: - - `OnDiscInserted()` checks `discType`: "Audio", "Video", "Title", "unknown" - - `OnDiscRemoved()` stops playback, returns to launcher -3. For "Video": calls `StartDVDPlayer()` which sets `theDVDPlayerInline.visible = true` -4. This loads `dvd.xap` via Inline node (lazy load) -5. dvd.xap's `onLoad()` calls `theDVDLevel.GoTo()` to transition to the player scene -6. Script calls `theDVDPlayer.play()`, `.stop()`, `.pause()`, etc. on the DVDPlayer node - -On the original Xbox, video frames came from a hardware MPEG-2 decoder that wrote to a D3D overlay surface. The `texture_panel` mesh was a black placeholder that the decoder composited over. On desktop, libmpv renders frames to an OpenGL FBO texture that gets bound to the `texture_panel` material during rendering. - -### dvd.xap Scene Structure - -Pure 3D scene with these key elements: -- `texture_panel`: flat mesh with material "Black80", no ImageTexture. This is where the video frame appears. -- `text_panel`: flat mesh for time/chapter text overlay -- `theText`: Text node for elapsed/chapter display -- `DVD_buttons` group with 6 button meshes (rewind, play, stop, fast-forward, prev chapter, next chapter) -- `dvd_shell`: panel frame meshes -- `icons` group: transport control icon meshes (play/pause/stop symbols as geometry) - -## Desktop Implementation - -### Architecture +## The pieces ``` -File > Open Media - | - v -MediaPlayer_Open(path) --> mpv_command("loadfile", path) - | - v -Set theDiscDrive.discType = "Video" - | - v -XAP reacts: dvd.xap loads, DVDPlayer.play() called - | - v -Each frame: - MediaPlayer_Update() --> mpv_render_context_render() to FBO - CDVDPlayer::Advance() --> reads mpv position/chapter/track info - updates XAP properties (hours, minutes, etc.) - D3D8 draw call --> detects "Black80" material on texture_panel - binds video FBO texture instead of black +Main menu button -> mediaplayer.xap scenes (browse, action menu, seasons and episodes) + | + v + MediaCollection node (media_db.cpp: scan, cache, TMDB, queries) + | PlayMovie / PlayEpisode + v + media_ui.cpp (fullscreen video, OSD, track menu, queue) + | + v + media_player.cpp (libmpv, software render, bgfx texture) ``` -Three files handle the integration: +Plex and Jellyfin follow the same shape with their own nodes (`PlexLibrary`, `JellyfinLibrary`) and scenes (`plex.xip`, `jellyfin.xip`), streaming from the server instead of reading files. -- **media_player.h/.cpp**: libmpv wrapper. Creates an mpv instance with OpenGL render context, renders video frames to an FBO, exposes transport controls (play/pause/stop/seek), chapter navigation, audio/subtitle track cycling, zoom/pan, A-B looping, frame stepping, and playback speed. The FBO texture ID is returned via `MediaPlayer_GetVideoTexture()` for the renderer to bind. +## Getting there -- **CDVDPlayer** (desktop_nodes.cpp): The XAP node implementation. Bridges all 34 DVDPlayer script functions to `MediaPlayer_*` calls. The `Advance()` method runs every frame, querying mpv for current position, chapter, track info, and updating the node's properties so the XAP OSD text stays current. +On the desktop build the main menu's Xbox Live button is pointed at the media library. `Configs/config.ini` holds the main menu wiring, and its `Button3Action=GoToMedia()` makes that button run `GoToMedia()` from the desktop `default.xap`, which loads the media player scenes. The scripts are in `xips-source/mediaplayer/` and the packed archives are `mediaplayer.xip` and `mediaplayer_action.xip` in `Data/Xips/`. -- **dvd.xap** (original Xbox script): Defines the joystick handlers that map controller buttons to DVDPlayer functions, the OSD layout, and the menu overlay. Unmodified from the Xbox version. +## The library: MediaCollection -### Video Rendering +`CMediaCollection` (in `theseus/desktop/media_db.cpp`) owns the library. It scans the Movies and TV roots set in Settings > Media > Local Library: -The video texture injection happens in the D3D8 translation layer. When the renderer encounters the `texture_panel` mesh (identified by its "Black80" material), it checks if `MediaPlayer_GetVideoTexture()` returns a valid GL texture. If so, it binds that texture instead of the material's default. When playback stops, the override is removed and the mesh goes back to black. +- **Movies**: one folder per movie (the first video file in it, or one level down), or video files straight in the root +- **TV**: one folder per show, with `Season N` or `Specials` folders inside, or episodes straight in the show folder. Episodes sort by `S01E02` style numbers, then by name +- **Video types**: `.mkv`, `.mp4`, `.m4v`, `.avi`, `.mov`, `.webm` -The CRT post-processing shader works on top of this: scanlines and curvature are applied to the final composited frame including video, giving media playback the same retro look as the rest of the dashboard. +Results are cached in `Library/MediaDB.cache` so the next launch doesn't rescan. With a TMDB API key the scan also fetches posters, synopses and ratings; without one the library still works, just with plain titles. -### Playback Modes +The scenes talk to it through script functions: `GetMovieCount`, `GetMovieTitle`, `SetCurrentShow`, `GetSeasonCount`, `GetEpisodeTitle`, `GetSelectedPlot`, `PlayMovie`, `PlayEpisode`, `RefreshLibrary` and friends. The scripts never touch libmpv directly. -The DVDPlayer node's `playbackMode` property maps to these states (from the Xbox DVD navigator): +## Playback: media_ui and media_player -| Mode | Value | Meaning | -|------|-------|---------| -| DPM_STOPPED | 0 | Stopped / no media | -| DPM_PAUSED | 1 | Paused | -| DPM_STILL | 2 | Still frame | -| DPM_PLAYING | 3 | Normal playback | -| DPM_NONE | 4 | No mode | -| DPM_SCANNING | 5 | Fast forward / rewind | -| DPM_TRICKPLAY | 8 | Frame stepping | +`PlayMovie` and `PlayEpisode` hand a path and a title to `media_ui.cpp`, which takes over the screen: video fullscreen, an on screen display in the dashboard's style, an audio and subtitle track picker, and a play queue for playlists. Esc, Q or Backspace stops and returns you to the library scene. -### Scan Speed Ramp +`media_player.cpp` wraps libmpv. Every play creates a fresh mpv instance and every stop tears it down completely, because partial stops left the render pipeline wedged on Apple Silicon. mpv renders each frame in software into a CPU buffer, which is uploaded to a bgfx texture and drawn with a fullscreen blit. That keeps video independent of whichever GPU backend bgfx picked, and lets the CRT effect apply to video too. Hardware decoding is a toggle in Settings > Display. -Forward/backward scan cycles through speed multipliers on each press: -- **Playing + forward scan**: 2x, 4x, 8x, 16x, 32x (via mpv `speed` property) -- **Paused + forward scan**: Slow motion 1/2x, 1/4x, 1/8x, 1/16x -- **Backward scan**: Simulated via periodic reverse seeking in Advance() (mpv doesn't support native reverse playback) +mpv brings its own audio output (CoreAudio, PulseAudio or ALSA, WASAPI). Dashboard sounds and music are muted while video plays and come back when it stops. -### Track Info Mapping +## Playlists -| Xbox property | mpv source | -|---------------|------------| -| audioFormat | `audio-codec-name` mapped to enum (0=AC3, 1=MPEG1, 2=MPEG2, 3=PCM/Stereo, 4=DTS) | -| audioChannels | `audio-params/channel-count` | -| audioLanguage | `current-tracks/audio/lang` | -| subTitleLanguage | `current-tracks/sub/lang` | -| subTitle | `sid` (subtitle track index) | +The Playlist Maker (F6) builds playlists out of the media library. They're stored as file paths in `Configs/playlists.ini`, so they survive rescans and renamed metadata, and `PlaylistCollection` exposes them to scripts. During playback `[` and `]` step through the queue. -### Function Mapping +## The DVDPlayer node -| Xbox function | mpv equivalent | -|---------------|---------------| -| play/pause/stop | `pause` property, `stop` command | -| forwardScan/backwardScan | `speed` property (forward), periodic seeking (backward) | -| stopScan | Reset `speed` to 1.0 | -| frameAdvance/frameReverse | `frame-step` / `frame-back-step` commands | -| nextChapter/prevChapter | `add chapter +/-1` | -| nextAudioStream | `cycle audio` | -| nextSubtitle | `cycle sub` | -| setScale | `video-zoom` (log2 scale) | -| setZoomPos | `video-pan-x` / `video-pan-y` | -| abRepeat | `ab-loop-a` / `ab-loop-b` properties | +The dashboard's own DVD scene, `dvd.xap`, drives a `DVDPlayer` node: play, pause, scan, chapters, angles, subtitles, A/B repeat, zoom, and a stream of callbacks (`OnTimeChange`, `OnChapterChange` and so on) that the on screen display listens to. The desktop `DVDPlayer` still honors that whole contract and maps it onto libmpv: scans become playback speed (reverse scan is periodic seeking backward, since mpv can't play in reverse), chapters map to mpv chapters, and audio and subtitle stream changes cycle mpv's tracks. Its per frame update fires the callbacks in the same order the 5960 binary does. -Functions that are no-ops for file playback: menu, titleMenu, selectUp/Down/Left/Right, activate, goUp, nextAngle, enableWideScreen, disableWideScreen, refreshAudioSettings. +On desktop the disc drive only ever reports audio CDs, though, so in normal use nothing opens `dvd.xap`. The node is there so the scene and its scripts keep loading and stay correct; the media library above is how you actually watch things. -### Audio +## Audio CDs -mpv has its own audio output (CoreAudio on macOS, ALSA/PulseAudio on Linux, WASAPI on Windows). Dashboard audio uses SDL_mixer separately. When media playback starts, dashboard background music is muted via `DashAudio_MuteAll()` and restored on stop via `DashAudio_UnmuteAll()`. +`DiscDrive` on desktop polls the first optical drive for an audio CD and reports it to the dashboard, which opens the music screen the way the Xbox did. Detection works on Linux and Windows (not macOS yet), and playback goes through libmpv. diff --git a/docs/desktop/porting-to-pc.md b/docs/desktop/porting-to-pc.md index 4b68c70..e668239 100644 --- a/docs/desktop/porting-to-pc.md +++ b/docs/desktop/porting-to-pc.md @@ -1,249 +1,91 @@ # Porting to PC -## The Challenge +## The challenge -The Xbox dashboard source code is approximately 25,000 lines of C++ that assumes it's running on an Xbox. Every file I/O call uses `CreateFile`/`ReadFile`. Every draw call goes through Direct3D 8. Every input read polls Xbox gamepads via XInput. The filesystem uses drive letter mappings (`Q:\Xips\default.xip`) from the modded Xbox ecosystem. Types are Windows-specific (`DWORD`, `HANDLE`, `BYTE`). +The dashboard was written to run on exactly one machine. Files open through `CreateFile` on drive letters (`Q:\Xips\default.xip`, `C:\UIX Configs\config.ini`). Every pixel goes through Xbox Direct3D 8. Input comes from four controller ports and a DVD remote. Sound goes through DirectSound and the console's audio DSP. Types are Win32 types (`DWORD`, `HANDLE`, `TCHAR`), and the kernel answers questions no PC has an answer for, like which memory units are plugged in. -The goal: make all of this run on macOS, Linux, and Windows with SDL2 and OpenGL: then strip out the Xbox scaffolding entirely. +UIX Desktop keeps the dashboard's engine code and its scripts as they are, and gives them an Xbox shaped world to live in on macOS, Linux and Windows. -## Strategy: One Tree, Targeted Divergence - -The initial UIX Desktop port lived in a separate repo and forked aggressively to escape the `#ifdef` swamp. That made desktop a clean codebase but meant every Xbox-side fix had to be re-ported by hand. The two trees were merged into the Theseus repo on 2026-04-10 and have been converging ever since. - -The current shape: one repo, four source roots that share whatever doesn't need to diverge. +## One tree, two builds ``` theseus/ - engine/ Pure logic, no platform deps (VM, nodes, math, settings, dotfield, locale) - shared/ Cross-platform with Win32 types (file I/O, titles, audio, screen, settings file) - render/ D3D8/graphics (scene graph, textures, materials, shape rendering, hud) - xbox/ Xbox-only (XTL init, modchip, kernel APIs, main.cpp) - desktop/ Desktop-only (SDL/OpenGL platform layer, ImGui tools, stubs, platform_shim.h) -``` - -Every file that compiles cleanly on both targets lives in `engine`/`shared`/`render`. Files that genuinely diverge (joystick goes to XInput on Xbox vs. SDL2 on desktop, audio goes to DirectSound vs. SDL_mixer, etc.) live under `xbox/` or `desktop/` and are picked up by their target's build only. The Xbox build is `make` (cross-compiles via clang+lld+cxbe to a bootable XBE); the desktop build is `make desktop` (native macOS/Linux) or `make desktop-win64` (mingw cross-compile to a Windows .exe). Same Makefile. - -The migration from "everything under `desktop/`" to "shared canonical source where possible" is ongoing. As of 2026-05-02 the engine, scene graph, rendering, and asset-loading code (including `xap_compile.cpp`, `xap_vm.cpp`, `tmap_system.cpp`, `asset_loader.cpp`, the node system, and the leaf nodes for screen, date, settings, hud, dotfield, fileutil, activefile, etc.) lives in `engine/`, `shared/`, and `render/` and is consumed by both builds. The remaining drift between the two builds sits in the desktop-only platform tooling (SDL window, ImGui developer tools, libmpv media player, HDD image browser) and a small set of files still being moved. - -The port is a mix of techniques depending on what each subsystem required: - -- **Virtualization**: The filesystem layer intercepts Win32 file I/O calls and redirects them to a local directory structure. The engine calls `CreateFile("Q:\\Xips\\default.xip")` and gets back a valid handle without knowing it's actually opening `./Data/Xips/default.xip` via `fopen()`. -- **Reimplementation**: D3D8 rendering was completely rebuilt on top of OpenGL. The engine calls the same `IDirect3DDevice8` interface, but every method is backed by OpenGL state and GLSL shaders. Audio was fully replaced with SDL_mixer. -- **Type compatibility**: Win32 types (`DWORD`, `HANDLE`, `BYTE`, etc.) are typedef'd to standard C types so the code compiles on non-Windows platforms. -- **Stubbing**: Xbox-only subsystems (Xbox Live, hardware management, etc.) are replaced with no-op stubs that satisfy the linker. Node types referenced by XAP scripts but irrelevant on desktop get stub classes so the script VM doesn't fail on unknown types. -- **Direct rewrite**: The input system (CJoystick) was rewritten from XInput to direct SDL2 calls. The EEPROM config system was replaced with hardcoded desktop defaults. Debug infrastructure (TRACE/ASSERT) was rewired to stderr. All dead Xbox code paths were removed. - -``` -Xbox Code: CreateFile("Q:\\Xips\\default.xip", ...) - | - v -Platform Layer: XboxFS_CreateFileA() translates path to "./Data/Xips/default.xip" - Opens with fopen(), returns a HANDLE-compatible wrapper - | - v -Xbox Code: ReadFile(handle, buffer, bytesToRead, &bytesRead, NULL) - | - v -Platform Layer: Calls fread() on the underlying FILE* -``` - -The filesystem example above is genuine virtualization: the engine doesn't know it's not on an Xbox. But other parts of the port are more hands-on. The D3D8 translation layer, for instance, isn't just intercepting calls. It's a from-scratch OpenGL renderer that happens to expose a D3D8-shaped interface. - -## The Platform Layer - -### sdl_platform.h: The Type Foundation - -This header makes non-Windows platforms speak Windows. It defines every Win32 type the Xbox code uses: - -```cpp -typedef uint32_t DWORD; -typedef uint8_t BYTE; -typedef uint16_t WORD; -typedef int32_t LONG; -typedef int BOOL; -typedef void* HANDLE; -typedef void* LPVOID; -typedef const char* LPCSTR; -// ~100 more type definitions + engine/ Pure logic: script compiler, math, locale, dotfield, settings nodes + shared/ Both builds: script VM, file and title handling, settings files, keyboard, screen + render/ Both builds: scene graph, materials, shapes, text, HUD, dynamic textures + xbox/ Xbox only: XTL init, kernel and hardware glue, DirectSound, XInput + desktop/ Desktop only: SDL main loop, D3D8 to bgfx shim, host layer, tools, media, launchers + toolbox/ Xbox only: FTP, drive and network tools from the PrometheOS lineage ``` -It also implements Win32 API functions using POSIX/SDL equivalents: -- `CreateFileA()` / `ReadFile()` / `WriteFile()` / `CloseHandle()`: file I/O via `fopen`/`fread`/`fwrite`/`fclose` -- `FindFirstFileA()` / `FindNextFileA()` / `FindClose()`: directory enumeration via `opendir`/`readdir`, with virtual game injection from `games.ini` -- `GetFileAttributesA()` / `GetFileAttributesExA()`: file stat via `stat()` -- `GetTickCount()`: millisecond timer via `SDL_GetTicks()` -- `Sleep()`: via `SDL_Delay()` -- `GlobalMemoryStatus()`: real host memory via `sysctl` (macOS) or `sysinfo` (Linux) -- `OutputDebugString()`: via `fprintf(stderr, ...)` - -Xbox-specific APIs that originally lived here have been removed entirely. The Xbox EEPROM configuration system (`XQueryValue`/`XSetValue`) was replaced by hardcoding sensible defaults directly in `config.cpp` (stereo audio, no parental controls, English language). The XInput gamepad bridge was replaced by direct SDL2 calls in `joystick.cpp`. Hardware monitoring (SMBus), memory unit management, soundtrack database, power management, and DirectSound were all removed after confirming zero callers. - -On Windows, most of these types already exist (they're the real Win32 types). The challenge there is that `DWORD` on MSVC is `unsigned long` while our typedef makes it `uint32_t` (`unsigned int`). Same size, but MSVC considers them different types for pointer compatibility. This required a `LPDW()` macro to bridge the gap at API boundaries. - -### xboxfs.h: The Virtual Filesystem - -The Theseus codebase uses drive letter mappings from the modded Xbox ecosystem: `C:\`, `E:\`, `Q:\Xips\`, etc. (These are scene conventions, not how retail Xbox addresses partitions internally.) On desktop, these collapse to three top-level folders that live next to the binary: - -``` -Configs/ (Xbox C:\UIX Configs\) - version - config.ini - games.ini - desktop.ini -Data/ (Xbox Q:\) - Xips/ - default.xip - games.xip - settings.xip - ... - Skins/ - Stock/ - Fonts/ - xbox.xtf - tahoma.ttf - System/ -Library/ (Xbox E:\) - Games/ - UDATA/ - Applications/ - Emulators/ -``` - -`F:\`, `G:\`, `R:\` (extra HDD partitions on real Xbox) silently fail to resolve on desktop -- there's only one library partition. XAP scripts that walk multiple drives just see empty results for the missing ones, so no duplicates. - -`xboxfs.h` provides: - -1. **Path translation**: `XboxFS_TranslatePath("Q:\\Xips\\default.xip")` returns `"Data/Xips/default.xip"` -2. **Case-insensitive lookup**: Xbox filenames are case-insensitive (NTFS). macOS is too (by default), but Linux is case-sensitive. The filesystem layer does case-insensitive directory scanning when a direct match fails. -3. **API interception**: `#define CreateFile CreateFileA` and then `#define CreateFileA XboxFS_CreateFileA`, so every file open in the Xbox code goes through the virtual filesystem without changing a single line of Xbox source. -4. **FATX fallback**: For UDATA/TDATA paths, the filesystem can read directly from a qcow2 Xbox HDD image via the FATX reader. - -On Windows, this required extra care: Win32's `CreateFile` is a real function, so the wrapper must strip Xbox-specific flags (`FILE_FLAG_NO_BUFFERING`, `FILE_FLAG_OVERLAPPED`) and convert forward slashes back to backslashes before calling the real Win32 API. - -### d3d8_sdl.h: D3D8 to OpenGL - -This is the largest and most complex part of the platform layer (~2,600 lines). It provides full implementations of: - -- `IDirect3D8`: device enumeration (returns one "SDL OpenGL" adapter) -- `IDirect3DDevice8`: the main rendering interface with GLSL shaders, render state tracking, vertex transforms, lighting, fog -- `IDirect3DTexture8`: texture objects backed by OpenGL textures -- `IDirect3DVertexBuffer8`: vertex data with CPU-side falloff computation -- `IDirect3DIndexBuffer8`: index buffers for mesh rendering -- D3DX math library: matrix, vector, and quaternion operations - -See [D3D8 to OpenGL Translation](d3d8-translation.md) for the full technical details. - -### desktop_stubs.cpp: The Missing Modules - -Many Xbox modules are irrelevant on desktop and are excluded from compilation entirely. But the remaining compiled code still references their symbols. `desktop_stubs.cpp` provides no-op implementations for: title collection management (`CTitleArray`), disc management (`DiscManager`), the dashboard's `net::` networking wrapper, Discord RPC configuration, and the XBE executable reader (which serves virtual game metadata from `games.ini` on desktop). - -### desktop_nodes.cpp: Script-Visible Nodes - -The XAP scripts reference node types that don't exist natively on desktop. If the script VM encounters an unknown node type, it fails. `desktop_nodes.cpp` provides implementations using the same `DECLARE_NODE`/`IMPLEMENT_NODE` pattern as the Xbox source. - -**Fully functional nodes** (real native implementations): -- `CAudioClip`: sound effects and music via SDL_mixer, with Xbox IMA ADPCM decoding -- `CSavedGameGrid`: enumerates real Xbox save data from xboxfs and qcow2 FATX images, renders the memory browser with scrolling title pods and save icons -- `CDVDPlayer`: full media playback via libmpv with transport controls, chapter navigation, A-B loop, frame stepping, zoom, and CRT post-processing -- `CMusicCollection`: soundtrack browsing from `Data/Music/` -- `CAudioVisualizer`: FFT visualization from PCM audio capture -- `CMemoryMonitor`: reports real host memory -- `CHardDrive`: drive info with virtual game library integration -- `CTheseusLauncher`: game launching via `system()` / `execv()` - -**Stub nodes** (registered so XAP scripts don't crash, return defaults): -- `CXboxLive`, `CLiveAccounts`: Xbox Live account management UI -- `CRecovery`: Xbox system recovery -- `CCopyDestination`: memory unit save data copy -- `CDiscDrive`: physical disc detection - -### audio_sdl.cpp: Sound Without DirectSound - -The Xbox used DirectSound with a custom DSP for audio. The desktop port replaces this entirely with SDL_mixer: - -- `.wav` files are loaded directly via `Mix_LoadWAV()` -- Xbox IMA ADPCM audio (the format used in XIP archives) is decoded in software to PCM before playback -- Music playback uses `Mix_PlayMusic()` for background tracks -- Sound effects use `Mix_PlayChannel()` with mixing -- A PostMix callback captures PCM samples for the audio visualizer +`build/Makefile` builds the desktop app from `desktop/` plus the shared roots. `build/Makefile.xbox` builds the XBE from `xbox/` plus the same shared roots. A file lives in a shared root when one copy can serve both builds; where the two genuinely differ (input, audio, file I/O, a handful of nodes whose Xbox version talks to hardware), each build has its own. `shared/platform.h` says which build is compiling: exactly one of `_XBOX` or `THESEUS_DESKTOP`, and on desktop exactly one host OS. `_WIN32` is true on the Xbox too, so nothing outside the host layer is allowed to test it. -The IMA ADPCM decoder was written from the format specification. It's a standard codec, just packaged in Xbox's custom container format. +## The platform layer -## Input: From XInput to SDL2 +### platform_shim.h: Xbox names, no OS headers -The Xbox dashboard used Microsoft's XInput API to handle gamepads (4-port, pressure-sensitive face buttons, analog triggers) and the DVD IR remote control kit. `CJoystick::Advance()` looped over all 4 ports every frame, polling device state and firing XAP callbacks (OnADown, OnMoveUp, OnReverse, OnPlay, etc.). +Dashboard code expects Win32 and XDK names: `HANDLE`, `ReadFile`, `GetTickCount`, `FILETIME`, `FindFirstFile`, `OutputDebugString` and friends. `platform_shim.h` provides them on every desktop OS, backed by SDL, the C runtime and the host layer. It's the same file on macOS, Linux and Windows, and it never includes an OS header. That last part matters most on Windows, where the real `CreateFile` exists and would happily accept `Q:\Xips\default.xip`. -On desktop, `CJoystick` talks directly to SDL2. A single `JoySnapshot` struct captures the merged state of any connected SDL_GameController plus the keyboard. The keyboard mapping (arrows, enter, backspace, WASD, etc.) is applied with OR logic so both input sources work simultaneously. Media keys (`[`, `]`, `I`, `;`, `'`, `0-9`) use poll-based edge detection to fire the same callbacks the IR remote did on Xbox. +### host.h and host_*: the only code that talks to the OS -All XAP-visible callbacks, the secret key sequence system, screen saver reset, and typomatic repeat logic are preserved identically. +`host.h` is a narrow door with plain types: make a folder, stat a file, list a directory, flush to disk, rename over, spawn a process, open a URL, find the executable, show a folder in the file manager, read the CD table of contents. `host_posix.cpp` and `host_posix_app.cpp` implement it for macOS and Linux, `host_win.cpp` and `host_win_app.cpp` for Windows. Those four files are the only ones that include OS headers or test OS macros. -## What Works vs What's Stubbed +Two build targets keep it that way: -**Fully functional on desktop:** -- XIP/XAP engine (scripting VM, scene graph, asset loading) -- 3D rendering (D3D8 reimplemented on OpenGL) -- Audio (SDL_mixer with Xbox IMA ADPCM decoder) -- Media playback (libmpv through the Xbox DVD player UI) -- Skin system (switchable from dashboard settings) -- Filesystem (virtualized to local directory with FATX fallback) -- Input (keyboard + controller via SDL2 GameController) -- Configuration (same config.ini as Xbox, desktop defaults for hardware settings) -- Game launching, Steam import, Title Maker, XAP editor, scene inspector, HDD browser -- Debug infrastructure (TRACE/ASSERT always active, no gating) +- `make lint-platform` fails on a Windows header, an OS macro or a host test anywhere else. CI runs it on every push. +- `make win64-import-guard` compiles the Windows objects and fails if anything outside `host_win*` imports kernel32's file, clock or sleep calls, which would mean real Win32 answered where the shim was supposed to. -**Stubbed (exists so XAP scripts don't crash, returns defaults):** -- Xbox Live / Live Accounts, Recovery, Copy Destination, Disc Drive +### xbox_path.h: drive letters -## The Windows Problem +Every Xbox path goes through one translator. Drive letters map onto the per user data folder: -On macOS and Linux, the platform layer works cleanly because none of the Win32 APIs exist; everything goes through the stubs. On Windows, it's a different story. +| Xbox | Desktop | +|---|---| +| `C:` (`C:\UIX Configs\`) | `Configs/` | +| `Q:` (the dashboard's own data) | `Data/` | +| `E:` | `Library/` | +| `X:` | `Library/` too: a desktop only drive that can never collide with a real Windows `C:\` path | +| `F:`, `G:`, `R:` and the rest | nothing | -Windows actually has `CreateFile`, `ReadFile`, `DWORD`, `HANDLE`, and all the other Win32 types. The Xbox code compiles against the *real* Win32 implementations instead of going through the virtualization layer. This causes: +Scripts that walk every partition just find the extra ones empty. Linux cares about case and the Xbox didn't, so `path_ci.h` resolves paths case insensitively when the exact spelling isn't on disk; skins and XAPs written on one OS keep working on another. -1. **Type mismatches**: `DWORD` is `unsigned long` on MSVC but `uint32_t` (unsigned int) in our typedefs. Same size, but MSVC treats them as incompatible types for pointers and references. +### app_paths: the user data folder -2. **Path translation bypass**: `CreateFile("Q:\\Xips\\default.xip")` calls the real Win32 `CreateFile`, which doesn't know about the xboxfs directory mapping. +The data lives in a per user folder (`~/Library/Application Support/Theseus` on macOS, `$XDG_DATA_HOME/theseus` or `~/.local/share/theseus` on Linux, `%APPDATA%\Theseus` on Windows). On first run it's seeded from the `Configs/`, `Data/` and `Library/` that ship beside the binary (inside a macOS `.app`, from `Contents/Resources`). Only `Data/shaders/` stays with the app, so a read only install works. -3. **Flag incompatibility**: Xbox-specific `CreateFile` flags (`FILE_FLAG_NO_BUFFERING`, `FILE_FLAG_OVERLAPPED`) get passed to the real Win32 API and cause unexpected behavior. +Updates to shipped `Data/` reach existing installs. `Data/.shipped` remembers the size and timestamp of every file it copied, both as shipped and as it looked right after. A copy that still matches is replaced when the shipped file changes; a copy the user edited is theirs and stays. For installs from before the manifest existed, the engine content (`Xips`, `System`, `Fonts`, `Language`) is replaced when it differs and the old copy is kept as `.old`; skins, music and the rest are left alone. -4. **Shell behavior**: `system("program.exe \"arg with spaces\"")` goes through `cmd.exe` on Windows, which strips the inner quotes. Replaced with `CreateProcess` for game launching. +Settings writes go through `safe_save.h`: write `.new`, flush it to disk, keep the current file as `.old`, then rename the new one into place. A crash or power cut mid save leaves either the old file or the new one, never half of one. -The fix was to intercept Win32 APIs on Windows too (`#define CreateFileA XboxFS_CreateFileA`), so even on Windows, file operations go through the virtual filesystem. A `LPDW()` macro bridges `uint32_t*` to `DWORD*` at Win32 API call sites. +### xbox_file.cpp: files that only exist in games.ini -## Debugging +The dashboard finds games by listing folders under `E:\Games` and reading each `default.xbe`. On desktop most entries aren't folders at all: they're lines in `Configs/games.ini`, or live entries from ES-DE and Xbox Cloud Gaming. `xbox_file.cpp` makes them list like folders anyway, and their `default.xbe` opens as a synthesized header carrying the title name and ID. The dashboard's game grid never knows. -The original Xbox dashboard had debug infrastructure (`TRACE`, `ASSERT`, `DumpStack`) that relied on MSVC's CRT debug runtime (`_CrtDbgBreak`, `_CrtSetAllocHook`, `_heapchk`, etc.). The desktop port replaces these with portable equivalents: +## Rendering -- `TRACE()` outputs to stderr (stripping the Xbox `\001` severity prefix) -- `ASSERT()` prints file and line on failure -- `DumpHex()` prints hex dumps -- The script VM stores function names for stack traces +The dashboard's D3D8 calls land on a purpose built shim over bgfx: Metal on macOS, Direct3D 11 on Windows, Vulkan on Linux. That's its own document: [d3d8-translation.md](d3d8-translation.md). -Debug output is always active: no build-type gating. The `_DEBUG` preprocessor gates that used to control this throughout the codebase have been removed. Console output has been cleaned up to only show errors on startup (~8 lines vs the original ~100). +## Audio -## Build System +`audio_sdl.cpp` replaces DirectSound with SDL_mixer. Sound effects play as mixer chunks and background music as mixer music. Many dashboard sounds are Xbox IMA ADPCM WAVs (format tag `0x0069`), which SDL can't read, so they're decoded to 16 bit PCM at load time. A post mix hook feeds the PCM to the audio visualizer and projectM. Master volume and mute sit on top of all of it, and dashboard audio goes quiet while a video or stream plays (those have their own audio output) and while the window is in the background. -A single Makefile at `build/Makefile` drives every target. Different invocations select different output: +## Input -- **`make`**: Xbox debug XBE via clang + lld-link + cxbe (cross-compile from macOS/Linux). Reads from `theseus/xbox/` + the shared roots. -- **`make retail`**: same shape but `CONFIG=retail`. -- **`make desktop`**: native macOS or Linux desktop binary, GCC or Clang. Reads from `theseus/desktop/` + the shared roots. Uses system SDL2/SDL2_mixer/OpenGL. -- **`make desktop-win64`**: Windows .exe via mingw-w64 cross-compile from macOS/Linux. Same source list as `make desktop`. Pulls SDL2 + SDL2_mixer + GLEW from `~/cross/` (one-time setup is in the target preamble). -- **Native Windows / Visual Studio 2019/2022**: open `build/theseus-desktop.sln` and build x64 / Release. Mirrors the cross-compile output but built with `cl.exe`. Setup in [`build/README-vs.md`](../../build/README-vs.md). +`desktop/joystick.cpp` implements the dashboard's `Joystick` node on SDL2's GameController API, so Xbox, PlayStation, Switch Pro and other mapped pads all work. The keyboard is merged into the same snapshot: arrows for the D-pad, WASD for the left stick, Enter for A, Backspace for B, Tab and backtick for White and Black. The DVD remote's buttons (play, pause, skip, digits) map to keys and fire the same script callbacks the IR receiver did. Typematic repeat, the screen saver reset and the secret button sequences behave like the Xbox. -The codebase compiles with permissive flags (`-fpermissive`, `-fms-extensions` on Clang/GCC; `/permissive-` and a comprehensive `DisableSpecificWarnings` list on MSVC) because the original Xbox code uses legacy C++ patterns that modern compilers reject. +## Nodes -All source filenames are lowercase for cross-platform compatibility on case-sensitive filesystems (Linux). +The scripts reference node types by name, and an unknown node type fails the whole scene, so every node the XIPs use has to exist on desktop. -## Code Organization +- **Shared with the Xbox build**: the scene graph, animation, materials and shapes, text, the keyboard, settings, screen, date, and the script built ins. +- **Desktop versions of Xbox nodes**: `AudioClip`, `MusicCollection`, `SavedGameGrid` (reads real Xbox saves from `Library/UDATA` and from qcow2 images), `HardDrive`, `MemoryMonitor`, `DiscDrive` (audio CDs), `DVDPlayer` (libmpv), `Joystick`, `Config` and `TheseusLauncher`. +- **Kept so scripts load**: `XboxLive`, `LiveAccounts` and `CopyDestination` answer with harmless defaults; there's no Xbox Live or memory unit on a PC. +- **Desktop additions**: `MediaCollection`, `PlaylistCollection`, `PlexLibrary`, `JellyfinLibrary`, `XcloudLibrary`, `GamesCollection` and `Display`. They're new names alongside the originals, never replacements; see [xap-contract.md](../xap-contract.md). -The dashboard engine lives in four roots that share whatever doesn't need to diverge per platform: +## Logs and crashes -- `theseus/engine/`: pure logic, no platform dependencies (script VM internals, math, locale, dotfield, settings nodes) -- `theseus/shared/`: cross-platform with Win32 types (file I/O, titles, audio, screen, settings file parser, savegame grid) -- `theseus/render/`: D3D8/graphics nodes (scene groups, shape rendering, dynamic textures, materials, text, hud) -- `theseus/desktop/`: desktop-only code: SDL main loop, D3D8-to-OpenGL translation, ImGui tools (inspector, XAP editor, Title Maker, HDD browser, menu bar, media player), SDL audio backend, filesystem virtualization, the unified launch dispatcher +Everything the app prints goes to the terminal and to `theseus.log` in the user data folder, with the previous run kept as `theseus.prev.log`, so a launch from Finder, a desktop entry or Explorer still leaves a log someone can send. On Windows a launch from a terminal prints there instead of the file. Near the top, the log names the shipped folder, the user data folder and the `games.ini` it's reading, and warns if the shader folder is missing. A crash handler prints a stack trace on the way down. -Within each root, every major tool is its own `.cpp`/`.h` pair. Shared state between ImGui panels is declared in `theseus/desktop/panel_shared.h`. +## Building -The Xbox build (`make`) reads from `xbox/` + the shared roots; the desktop build (`make desktop`) reads from `desktop/` + the shared roots. Files migrate from `desktop/` into the shared roots whenever a desktop file converges with the canonical decompiled form: one source serves both targets where the platform divergence allows it. +See [building.md](building.md) and [dependencies.md](dependencies.md). diff --git a/docs/xap-contract.md b/docs/xap-contract.md index 4dc1af5..59ed8e1 100644 --- a/docs/xap-contract.md +++ b/docs/xap-contract.md @@ -2,7 +2,9 @@ This document catalogs every node type, function, property, and callback that XAP scripts in the dashboard XIPs reference. This is the "do not break" API surface; the C++ implementation behind these interfaces can be rewritten freely, but the names and behaviors must remain stable. -Generated by scanning all 39 .xap files across the dashboard XIP set. +Generated by scanning all 39 .xap files across the dashboard XIP set. Everything below is the retail dashboard's surface; the nodes Theseus adds are listed separately in [Nodes Theseus adds](#nodes-theseus-adds). + +New behavior never renames or rewrites one of these. It goes in alongside, as a new node or a new function, so stock XIPs and community skins keep loading. ## Node Types (32 unique) @@ -424,3 +426,20 @@ DisappearAfter(seconds) ### Box (inline geometry, no DEF'd attributes) + +## Nodes Theseus adds + +These are new node types alongside the retail set, used by Theseus's own scenes in [`xips-source/`](../xips-source/README.md). A stock XIP never references them, and a scene that does only loads on a build that has them. + +| Node type | Build | Source | What it's for | +|-----------|-------|--------|---------------| +| TheseusLauncher | both | `xbox/theseus_launcher.cpp`, `desktop/desktop_nodes.cpp` | Script callable launcher for XBEs, `.uixshortcut` files and `games.ini` entries | +| TitleScanner | Xbox | `xbox/title_scanner.cpp` | Native title scan across drives, feeding the title menu | +| StarField | both | `render/starfield.cpp` | Parallax 3D star backdrop | +| GamesCollection | desktop | `desktop/desktop_nodes.cpp` | The `games.ini` library by category, refreshed as Title Maker edits it | +| MediaCollection | desktop | `desktop/media_db.cpp` | Movie and TV library, TMDB metadata, playback | +| PlaylistCollection | desktop | `desktop/media_db.cpp` | Playlists from the Playlist Maker | +| PlexLibrary | desktop | `desktop/desktop_nodes.cpp` | Plex browsing and playback | +| JellyfinLibrary | desktop | `desktop/desktop_nodes.cpp` | Jellyfin browsing and playback | +| XcloudLibrary | desktop | `desktop/xcloud_node.cpp` | Xbox Cloud Gaming titles and remote play consoles | +| Display | desktop | `desktop/desktop_nodes.cpp` | Window resolution and display mode, shared with Settings > Display | diff --git a/theseus/desktop/app_paths.cpp b/theseus/desktop/app_paths.cpp index dc4f0bb..4e826ec 100644 --- a/theseus/desktop/app_paths.cpp +++ b/theseus/desktop/app_paths.cpp @@ -7,18 +7,12 @@ #include #include #include +#include #include +#include +#include -#ifdef _WIN32 -#include -#include -#else -#include -#include -#endif -#ifdef __APPLE__ -#include -#endif +#include "host.h" // The only thing that stays with the binary. Nobody hand edits a .bin, and // packagers want it read only. @@ -30,26 +24,14 @@ const char* Plat_ExeDir() { if (s_init) return s_dir; s_init = true; -#ifdef _WIN32 - DWORD len = GetModuleFileNameA(NULL, s_dir, sizeof(s_dir)); - if (len > 0) { - char* last = strrchr(s_dir, '\\'); - if (last) *last = '\0'; - } -#elif defined(__APPLE__) - uint32_t sz = sizeof(s_dir); - if (_NSGetExecutablePath(s_dir, &sz) == 0) { - char* last = strrchr(s_dir, '/'); - if (last) *last = '\0'; - } -#else - ssize_t len = readlink("/proc/self/exe", s_dir, sizeof(s_dir) - 1); - if (len > 0) { - s_dir[len] = '\0'; + if (Host_ExePath(s_dir, sizeof(s_dir))) { char* last = strrchr(s_dir, '/'); + char* back = strrchr(s_dir, '\\'); + if (back > last) last = back; if (last) *last = '\0'; + } else { + s_dir[0] = '\0'; } -#endif if (!s_dir[0]) strcpy(s_dir, "."); return s_dir; } @@ -60,45 +42,14 @@ const char* Plat_UserDataDir() { if (s_init) return s_dir; s_init = true; -#ifdef _WIN32 - const char* appdata = getenv("APPDATA"); - if (appdata && *appdata) - snprintf(s_dir, sizeof(s_dir), "%s\\Theseus", appdata); - else - snprintf(s_dir, sizeof(s_dir), "%s\\AppData\\Roaming\\Theseus", PlatformHomeDir()); -#elif defined(__APPLE__) - snprintf(s_dir, sizeof(s_dir), "%s/Library/Application Support/Theseus", PlatformHomeDir()); -#else - const char* xdg = getenv("XDG_DATA_HOME"); - if (xdg && *xdg) - snprintf(s_dir, sizeof(s_dir), "%s/theseus", xdg); - else - snprintf(s_dir, sizeof(s_dir), "%s/.local/share/theseus", PlatformHomeDir()); -#endif + Host_UserDataDir(s_dir, sizeof(s_dir)); return s_dir; } const char* Plat_ShippedDir() { -#ifdef __APPLE__ - // Inside a .app the binary sits in Contents/MacOS and the payload in - // Contents/Resources. Loose builds keep everything beside the binary. static char s_dir[1024] = {0}; - static bool s_init = false; - if (!s_init) { - s_init = true; - const char* exe = Plat_ExeDir(); - size_t n = strlen(exe); - const char kTail[] = "/Contents/MacOS"; - size_t t = sizeof(kTail) - 1; - if (n > t && strcmp(exe + n - t, kTail) == 0) - snprintf(s_dir, sizeof(s_dir), "%.*s/Contents/Resources", (int)(n - t), exe); - else - snprintf(s_dir, sizeof(s_dir), "%s", exe); - } + if (!s_dir[0]) Host_ShippedDir(Plat_ExeDir(), s_dir, sizeof(s_dir)); return s_dir; -#else - return Plat_ExeDir(); -#endif } static bool IsUserOwned(const char* logical) { @@ -130,36 +81,149 @@ static void CopyTree(const char* src, const char* dst) { struct stat st; if (stat(src, &st) != 0) return; Plat_MkdirP(dst); -#ifdef _WIN32 - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", src); - struct _finddata_t fd; - intptr_t h = _findfirst(pattern, &fd); - if (h == -1) return; - do { - if (!strcmp(fd.name, ".") || !strcmp(fd.name, "..")) continue; + HostDir* dir = Host_OpenDir(src); + if (!dir) return; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; char s[1024], d[1024]; - snprintf(s, sizeof(s), "%s/%s", src, fd.name); - snprintf(d, sizeof(d), "%s/%s", dst, fd.name); - if (fd.attrib & _A_SUBDIR) CopyTree(s, d); - else CopyFile(s, d); - } while (_findnext(h, &fd) == 0); - _findclose(h); -#else - DIR* dir = opendir(src); + snprintf(s, sizeof(s), "%s/%s", src, name); + snprintf(d, sizeof(d), "%s/%s", dst, name); + if (info.attrs & HOST_ATTR_DIR) CopyTree(s, d); + else CopyFile(s, d); + } + Host_CloseDir(dir); +} + +// Data/ is shipped content, so updates have to reach installs that already +// have a copy. .shipped remembers what we copied (shipped size + mtime) and +// what the user's copy looked like right after. A copy that still matches is +// ours to replace; one the user changed is theirs and stays put. +struct ShippedRec { uint64_t shipSize; int64_t shipTime; uint64_t userSize; int64_t userTime; }; +static std::map s_shipped; +static bool s_shippedDirty = false; + +static std::string ShippedManifestPath() { + return std::string(Plat_UserDataDir()) + "/Data/.shipped"; +} + +static void LoadShippedManifest() { + FILE* f = fopen(ShippedManifestPath().c_str(), "r"); + if (!f) return; + char line[1400]; + while (fgets(line, sizeof(line), f)) { + char rel[1024]; unsigned long long ss, us; long long st, ut; + if (sscanf(line, "%llu %lld %llu %lld %1023[^\n]", &ss, &st, &us, &ut, rel) == 5) + s_shipped[rel] = { ss, st, us, ut }; + } + fclose(f); +} + +static void SaveShippedManifest() { + if (!s_shippedDirty) return; + std::string path = ShippedManifestPath(), tmp = path + ".tmp"; + FILE* f = fopen(tmp.c_str(), "w"); + if (!f) return; + for (const auto& kv : s_shipped) + fprintf(f, "%llu %lld %llu %lld %s\n", (unsigned long long)kv.second.shipSize, (long long)kv.second.shipTime, + (unsigned long long)kv.second.userSize, (long long)kv.second.userTime, kv.first.c_str()); + if (fclose(f) == 0) Host_RenameReplace(tmp.c_str(), path.c_str()); +} + +static bool SameFileBytes(const char* a, const char* b) { + FILE* fa = fopen(a, "rb"); + FILE* fb = fopen(b, "rb"); + bool same = fa && fb; + char ba[65536], bb[65536]; + while (same) { + size_t na = fread(ba, 1, sizeof(ba), fa), nb = fread(bb, 1, sizeof(bb), fb); + if (na != nb || memcmp(ba, bb, na) != 0) same = false; + if (na == 0) break; + } + if (fa) fclose(fa); + if (fb) fclose(fb); + return same; +} + +// Copy beside the target, then swap it in, so a crash never leaves half a file. +static bool ReplaceFile(const char* src, const char* dst) { + std::string part = std::string(dst) + ".part"; + remove(part.c_str()); + CopyFile(src, part.c_str()); + HostFileInfo pi; + if (!Host_Stat(part.c_str(), &pi)) return false; + if (!Host_RenameReplace(part.c_str(), dst)) { remove(part.c_str()); return false; } + return true; +} + +static void Remember(const std::string& rel, const HostFileInfo& ship, const char* dst) { + HostFileInfo ui; + if (!Host_Stat(dst, &ui)) return; + s_shipped[rel] = { ship.size, ship.mtime, ui.size, ui.mtime }; + s_shippedDirty = true; +} + +// Engine content: a stale copy of these breaks the dash, so on installs from +// before .shipped existed a differing copy is set aside as .old and replaced. +// Everything else (skins, music, orbs, screenshots) is left to the user. +static bool IsEngineContent(const std::string& rel) { + static const char* kDirs[] = { "Data/Xips/", "Data/System/", "Data/Fonts/", "Data/Language/" }; + for (const char* d : kDirs) + if (strncasecmp(rel.c_str(), d, strlen(d)) == 0) return true; + return false; +} + +static void SyncShippedFile(const char* src, const char* dst, const std::string& rel) { + HostFileInfo ship, user; + if (!Host_Stat(src, &ship)) return; + + if (!Host_Stat(dst, &user)) { + CopyFile(src, dst); + Remember(rel, ship, dst); + return; + } + + auto it = s_shipped.find(rel); + if (it != s_shipped.end()) { + const ShippedRec& r = it->second; + bool shipChanged = r.shipSize != ship.size || r.shipTime != ship.mtime; + bool userUntouched = r.userSize == user.size && r.userTime == user.mtime; + if (shipChanged && userUntouched && ReplaceFile(src, dst)) { + Remember(rel, ship, dst); + fprintf(stderr, "[Paths] updated %s\n", rel.c_str()); + } + return; + } + + // Copied before .shipped existed, so we can't tell who last touched it. + if (SameFileBytes(src, dst)) { + Remember(rel, ship, dst); + } else if (IsEngineContent(rel)) { + std::string old = std::string(dst) + ".old"; + if (Host_RenameReplace(dst, old.c_str()) && ReplaceFile(src, dst)) { + Remember(rel, ship, dst); + fprintf(stderr, "[Paths] updated %s (previous copy kept as .old)\n", rel.c_str()); + } + } +} + +static void SyncShippedTree(const char* src, const char* dst, const std::string& rel) { + Plat_MkdirP(dst); + HostDir* dir = Host_OpenDir(src); if (!dir) return; - struct dirent* e; - while ((e = readdir(dir)) != NULL) { - if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; char s[1024], d[1024]; - snprintf(s, sizeof(s), "%s/%s", src, e->d_name); - snprintf(d, sizeof(d), "%s/%s", dst, e->d_name); - struct stat es; - if (stat(s, &es) == 0 && S_ISDIR(es.st_mode)) CopyTree(s, d); - else CopyFile(s, d); + snprintf(s, sizeof(s), "%s/%s", src, name); + snprintf(d, sizeof(d), "%s/%s", dst, name); + std::string r = rel + "/" + name; + if (info.attrs & HOST_ATTR_DIR) SyncShippedTree(s, d, r); + else SyncShippedFile(s, d, r); } - closedir(dir); -#endif + Host_CloseDir(dir); } // Copy one tree into the user dir if it isn't there yet. @@ -189,39 +253,23 @@ void AppPaths_Init() { char dataDir[1024]; snprintf(dataDir, sizeof(dataDir), "%s/Data", Plat_ShippedDir()); Plat_MkdirP(AppPath_Tree("Data")); + LoadShippedManifest(); -#ifdef _WIN32 - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", dataDir); - struct _finddata_t fd; - intptr_t h = _findfirst(pattern, &fd); - if (h == -1) return; - do { - if (!(fd.attrib & _A_SUBDIR)) continue; - if (!strcmp(fd.name, ".") || !strcmp(fd.name, "..")) continue; - if (!strcasecmp(fd.name, "shaders")) continue; - char rel[1024]; - snprintf(rel, sizeof(rel), "Data/%s", fd.name); - SeedTree(rel); - } while (_findnext(h, &fd) == 0); - _findclose(h); -#else - DIR* dir = opendir(dataDir); + HostDir* dir = Host_OpenDir(dataDir); if (!dir) return; - struct dirent* e; - while ((e = readdir(dir)) != NULL) { - if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; - if (!strcasecmp(e->d_name, "shaders")) continue; - char probe[1024]; - snprintf(probe, sizeof(probe), "%s/%s", dataDir, e->d_name); - struct stat es; - if (stat(probe, &es) != 0 || !S_ISDIR(es.st_mode)) continue; - char rel[1024]; - snprintf(rel, sizeof(rel), "Data/%s", e->d_name); - SeedTree(rel); + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (!(info.attrs & HOST_ATTR_DIR)) continue; + if (!strcmp(name, ".") || !strcmp(name, "..")) continue; + if (!strcasecmp(name, "shaders")) continue; + char src[1024], dst[1024]; + snprintf(src, sizeof(src), "%s/%s", dataDir, name); + snprintf(dst, sizeof(dst), "%s/Data/%s", user, name); + SyncShippedTree(src, dst, std::string("Data/") + name); } - closedir(dir); -#endif + Host_CloseDir(dir); + SaveShippedManifest(); } const char* AppPathf(const char* fmt, ...) { @@ -251,23 +299,7 @@ const char* AppPath_Tree(const char* tree) { void Plat_OpenInFileManager(const char* dir) { if (!dir || !*dir) return; -#ifdef _WIN32 - ShellExecuteA(NULL, "open", dir, NULL, NULL, SW_SHOWNORMAL); -#else - // Fork so a slow file manager can't stall us, and no shell is involved. - pid_t pid = fork(); - if (pid == 0) { -#ifdef __APPLE__ - execlp("open", "open", dir, (char*)NULL); -#else - // Under flatpak we want the host's file manager. - if (getenv("FLATPAK_ID")) - execlp("flatpak-spawn", "flatpak-spawn", "--host", "xdg-open", dir, (char*)NULL); - execlp("xdg-open", "xdg-open", dir, (char*)NULL); -#endif - _exit(127); - } -#endif + Host_OpenFolder(dir); } const char* AppPath(const char* logical) { diff --git a/theseus/desktop/audio_sdl.cpp b/theseus/desktop/audio_sdl.cpp index 19b4d09..b169e64 100644 --- a/theseus/desktop/audio_sdl.cpp +++ b/theseus/desktop/audio_sdl.cpp @@ -14,16 +14,7 @@ #include #include #include -#ifdef _WIN32 -#include -#include -#ifndef S_ISDIR -#define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR) -#endif -#else -#include -#include -#endif +#include "host.h" #include "audio_sdl.h" #include "stb_image.h" @@ -200,12 +191,14 @@ static Mix_Chunk* LoadXboxADPCM(const char* path) } free(adpcmData); - // Build a Mix_Chunk with the PCM data - // We need to convert to the audio device format + // A chunk has to be in the mixer's own format, mono files included. + int mixHz = 44100, mixCh = 2; + Uint16 mixFmt = MIX_DEFAULT_FORMAT; + Mix_QuerySpec(&mixHz, &mixFmt, &mixCh); SDL_AudioCVT cvt; int built = SDL_BuildAudioCVT(&cvt, - AUDIO_S16LSB, channels, sampleRate, // source format - AUDIO_S16LSB, channels, 44100); // target (our mixer format) + AUDIO_S16LSB, channels, sampleRate, + mixFmt, mixCh, mixHz); uint8_t* finalBuf; uint32_t finalLen; @@ -418,7 +411,9 @@ int DashAudio_Init(void) int frames = g_audioBufferFrames; if (frames < 256) frames = 256; if (frames > 8192) frames = 8192; - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, frames) < 0) { + // No format changes allowed: SDL converts for the device, and the ADPCM + // chunks and the visualizer tap can count on 44.1 kHz 16 bit stereo. + if (Mix_OpenAudioDevice(44100, MIX_DEFAULT_FORMAT, 2, frames, NULL, 0) < 0) { fprintf(stderr, "[Audio] Mix_OpenAudio failed: %s\n", Mix_GetError()); return -1; } @@ -967,13 +962,9 @@ static bool IsM3UFile(const char* name) static long long FileMTime(const char* path) { - struct stat st; - if (stat(path, &st) != 0) return 0; -#ifdef __APPLE__ - return (long long)st.st_mtimespec.tv_sec; -#else - return (long long)st.st_mtime; -#endif + HostFileInfo info; + if (!Host_Stat(path, &info)) return 0; + return (long long)info.mtime; } static void ApplyDBToSong(const DBEntry& e, Song& s) @@ -1092,23 +1083,12 @@ int DashMusic_Scan(const char* musicRoot) soundtrack.songs.push_back(BuildSong(full, StripExtension(songName))); }; -#ifdef _WIN32 - char searchBuf[512]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", stPath.c_str()); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind != -1) { - do { ProcessSongFile(fd.name); } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); - } -#else - DIR* stDir = opendir(stPath.c_str()); + HostDir* stDir = Host_OpenDir(stPath.c_str()); if (stDir) { - struct dirent* songEntry; - while ((songEntry = readdir(stDir)) != NULL) ProcessSongFile(songEntry->d_name); - closedir(stDir); + char songName[260]; + while (Host_ReadDir(stDir, songName, sizeof(songName), NULL)) ProcessSongFile(songName); + Host_CloseDir(stDir); } -#endif if (!soundtrack.songs.empty()) { if (soundtrack.albumPath.empty() && !fallbackArt.empty()) @@ -1162,43 +1142,20 @@ int DashMusic_Scan(const char* musicRoot) looseMusic.songs.push_back(BuildSong(full, StripExtension(fileName))); }; -#ifdef _WIN32 - char searchBuf[512]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", musicRoot); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { - fprintf(stderr, "[Audio] Music directory '%s' not found (this is OK if no music installed)\n", musicRoot); - return 0; - } - do { - std::string entryPath = std::string(musicRoot) + "/" + fd.name; - struct stat est; - if (stat(entryPath.c_str(), &est) == 0) { - if (S_ISDIR(est.st_mode)) ProcessSoundtrackDir(fd.name); - else if (IsM3UFile(fd.name)) ProcessM3UFile(fd.name); - else ProcessLooseFile(fd.name); - } - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* rootDir = opendir(musicRoot); + HostDir* rootDir = Host_OpenDir(musicRoot); if (!rootDir) { fprintf(stderr, "[Audio] Music directory '%s' not found (this is OK if no music installed)\n", musicRoot); return 0; } - struct dirent* entry; - while ((entry = readdir(rootDir)) != NULL) { - if (entry->d_name[0] == '.') continue; - std::string entryPath = std::string(musicRoot) + "/" + entry->d_name; - struct stat est; - if (stat(entryPath.c_str(), &est) != 0) continue; - if (S_ISDIR(est.st_mode)) ProcessSoundtrackDir(entry->d_name); - else if (IsM3UFile(entry->d_name)) ProcessM3UFile(entry->d_name); - else ProcessLooseFile(entry->d_name); + char entryName[260]; + bool isDir; + while (Host_ReadDirType(rootDir, entryName, sizeof(entryName), &isDir)) { + if (entryName[0] == '.') continue; + if (isDir) ProcessSoundtrackDir(entryName); + else if (IsM3UFile(entryName)) ProcessM3UFile(entryName); + else ProcessLooseFile(entryName); } - closedir(rootDir); -#endif + Host_CloseDir(rootDir); if (!looseMusic.songs.empty()) { bool merged = false; diff --git a/theseus/desktop/cdaudio.cpp b/theseus/desktop/cdaudio.cpp index 47a5be2..fe2b31d 100644 --- a/theseus/desktop/cdaudio.cpp +++ b/theseus/desktop/cdaudio.cpp @@ -2,17 +2,17 @@ // // TOC detection: platform ioctls, no extra deps. // Linux : CDROMREADTOCHDR / CDROMREADTOCENTRY -// Windows: DeviceIoControl IOCTL_CDROM_READ_TOC +// Windows: IOCTL_CDROM_READ_TOC, in host_win_app.cpp // // Playback backends (first match wins): // UIX_CDAUDIO : libmpv audio-only handle -// Linux or Windows : CDROMREADAUDIO / IOCTL_CDROM_RAW_READ + SDL audio +// Linux : CDROMREADAUDIO + SDL audio // Anything else : stubs (detection still works) // -// The SDL ring-buffer backend is shared between Linux and Windows; -// only the reader-thread ioctl differs. #include "cdaudio.h" +#include "platform.h" +#include "host.h" #include #include #include @@ -25,7 +25,7 @@ // Platform: disc detection + TOC + track LBAs // ============================================================================ -#if defined(__linux__) +#if defined(THESEUS_HOST_LINUX) #include #include @@ -97,83 +97,15 @@ static CdDiscType PlatformReadTOC(int* outCount, int* outDurations, int maxTrack static void PlatformPopulatePlaybackInfo() {} // already done in PlatformReadTOC -#elif defined(_WIN32) - -#define WIN32_LEAN_AND_MEAN -#include -#include -#include - -// RAW_READ_INFO / IOCTL_CDROM_RAW_READ: defined locally so we don't depend -// on mingw-w64 version shipping them. -struct CdRawReadInfo { - LARGE_INTEGER DiskOffset; // LBA * 2048 - ULONG SectorCount; - ULONG TrackMode; // 2 = CDDA -}; -#ifndef IOCTL_CDROM_RAW_READ -// CTL_CODE(0x2, 0xF, METHOD_OUT_DIRECT=2, FILE_READ_ACCESS=1) -#define IOCTL_CDROM_RAW_READ 0x0002403E -#endif - -static char s_cdDriveLetter = 0; -static int s_trackStartLBA[MAX_CDTRACKS] = {}; -static int s_leadOutLBA = 0; - -static HANDLE OpenCdromDevice() -{ - char path[] = "\\\\.\\D:"; - for (char c = 'D'; c <= 'H'; ++c) { - char root[4] = { c, ':', '\\', 0 }; - if (GetDriveTypeA(root) != DRIVE_CDROM) continue; - path[4] = c; - HANDLE h = CreateFileA(path, GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); - if (h != INVALID_HANDLE_VALUE) { - s_cdDriveLetter = c; - return h; - } - } - return INVALID_HANDLE_VALUE; -} +#elif defined(THESEUS_HOST_WIN) static CdDiscType PlatformReadTOC(int* outCount, int* outDurations, int maxTracks) { if (outCount) *outCount = 0; - HANDLE h = OpenCdromDevice(); - if (h == INVALID_HANDLE_VALUE) return CD_NONE; - - CDROM_TOC toc; - DWORD br = 0; - BOOL ok = DeviceIoControl(h, IOCTL_CDROM_READ_TOC, - nullptr, 0, &toc, sizeof(toc), &br, nullptr); - CloseHandle(h); - if (!ok) return CD_NONE; - - int count = toc.LastTrack - toc.FirstTrack + 1; - if (outCount) *outCount = count; - bool allAudio = true; - for (int i = 0; i <= count; ++i) { // +1 to include lead-out - int ti = toc.FirstTrack - 1 + i; // index into TrackData - UCHAR* a = toc.TrackData[ti].Address; - int lba = ((int)a[1] * 60 + a[2]) * 75 + a[3] - 150; - if (lba < 0) lba = 0; - - if (i < count) { - if (toc.TrackData[ti].Control & 4) allAudio = false; - s_trackStartLBA[i] = lba; - if (outDurations && i + 1 <= count) { - UCHAR* b = toc.TrackData[ti + 1].Address; - int t0 = (int)a[1] * 60 + a[2]; - int t1 = (int)b[1] * 60 + b[2]; - outDurations[i] = (t1 > t0) ? (t1 - t0) : 0; - } - } else { - s_leadOutLBA = lba; - } - } + int count = Host_CdReadToc(outDurations, maxTracks, &allAudio); + if (count < 0) return CD_NONE; + if (outCount) *outCount = count; return allAudio ? CD_AUDIO : CD_DATA; } @@ -264,8 +196,8 @@ void CdAudio_Update() } } -// ---- B: SDL ring-buffer + platform raw-sector read (Linux + Windows) ------- -#elif defined(__linux__) || defined(_WIN32) +// ---- B: SDL ring-buffer + raw-sector read (Linux) ------------------------- +#elif defined(THESEUS_HOST_LINUX) #include @@ -314,7 +246,7 @@ static int SDLCALL ReaderFunc(void* arg) bool readOk = true; // ----- platform-specific device open + read ----- -#if defined(__linux__) +#if defined(THESEUS_HOST_LINUX) int fd = OpenDevice(); if (fd < 0) { fprintf(stderr, "[CdAudio] cannot open device\n"); @@ -350,47 +282,6 @@ static int SDLCALL ReaderFunc(void* arg) } close(fd); -#elif defined(_WIN32) - char devPath[16]; - snprintf(devPath, sizeof(devPath), "\\\\.\\%c:", s_cdDriveLetter ? s_cdDriveLetter : 'D'); - HANDLE hDev = CreateFileA(devPath, GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, OPEN_EXISTING, 0, nullptr); - if (hDev == INVALID_HANDLE_VALUE) { - fprintf(stderr, "[CdAudio] cannot open %s: error %lu\n", devPath, GetLastError()); - SDL_AtomicSet(&s_atomPlaying, 0); - return -1; - } - - while (!SDL_AtomicGet(&s_atomStop) && lba < ra.endLBA) { - if (SDL_AtomicGet(&s_atomPaused)) { SDL_Delay(20); continue; } - int n = CDDA_READ_BATCH; - if (lba + n > ra.endLBA) n = ra.endLBA - lba; - - CdRawReadInfo rri; - rri.DiskOffset.QuadPart = (LONGLONG)lba * 2048; - rri.SectorCount = n; - rri.TrackMode = 2; // CDDA - - DWORD br = 0; - if (!DeviceIoControl(hDev, IOCTL_CDROM_RAW_READ, - &rri, sizeof(rri), buf, n * CDDA_FRAME_BYTES, &br, nullptr)) { - fprintf(stderr, "[CdAudio] IOCTL_CDROM_RAW_READ at LBA %d: error %lu\n", lba, GetLastError()); - readOk = false; break; - } - - int bytes = n * CDDA_FRAME_BYTES; - SDL_LockMutex(s_mutex); - while (!SDL_AtomicGet(&s_atomStop) && (CDDA_RING_BYTES - s_ringFill) < bytes) - SDL_CondWait(s_cond, s_mutex); - if (!SDL_AtomicGet(&s_atomStop)) { - for (int i = 0; i < bytes; i++) { s_ring[s_ringWrite]=buf[i]; s_ringWrite=(s_ringWrite+1)%CDDA_RING_BYTES; } - s_ringFill += bytes; - } - SDL_UnlockMutex(s_mutex); - lba += n; - } - CloseHandle(hDev); #endif // platform read // ------------------------------------------------ diff --git a/theseus/desktop/client_util.h b/theseus/desktop/client_util.h index fa8c03d..2d5097e 100644 --- a/theseus/desktop/client_util.h +++ b/theseus/desktop/client_util.h @@ -7,22 +7,13 @@ #include #include #include -#ifdef _WIN32 -#include -#endif +#include "host.h" // Join a worker thread if it is running. inline void JoinIf(std::thread& t) { if (t.joinable()) t.join(); } -// Create a directory. POSIX mkdir takes (path, mode); the Windows CRT one -// takes just (path). -inline int Mkdir_(const char* path) { -#ifdef _WIN32 - return mkdir(path); -#else - return mkdir(path, 0755); -#endif -} +// Create a directory. 0 when it exists afterwards. +inline int Mkdir_(const char* path) { return Host_MakeDir(path) ? 0 : -1; } // Random RFC 4122 version-4 UUID as a 36-char string plus NUL. inline void GenerateUUID(char out[37]) { diff --git a/theseus/desktop/config.cpp b/theseus/desktop/config.cpp index d7e29c4..9c394f9 100755 --- a/theseus/desktop/config.cpp +++ b/theseus/desktop/config.cpp @@ -9,7 +9,7 @@ #include "runner.h" #include "timezone.h" -#include "dashlocale.h" +#include "locale_node.h" #include "xbe.h" #include "network.h" #include "config.h" @@ -365,35 +365,6 @@ void CConfig::SetDST(int bObserveDST) // Parental Controls / Password // ============================================================================ -static uint32_t EncodePassword(const char *szPassword) -{ - uint32_t dwPassword = 0; - for (const char *pch = szPassword; *pch != 0; pch += 1) - { - dwPassword <<= 4; - - switch (*pch) - { - case 'u': dwPassword += 1; break; // up - case 'd': dwPassword += 2; break; // down - case 'l': dwPassword += 3; break; // left - case 'r': dwPassword += 4; break; // right - case 'a': dwPassword += 5; break; // A - case 'b': dwPassword += 6; break; // B - case 'x': dwPassword += 7; break; // X - case 'y': dwPassword += 8; break; // Y - case 'B': dwPassword += 9; break; // black - case 'W': dwPassword += 10; break; // white - case 'L': dwPassword += 11; break; // L trigger - case 'R': dwPassword += 12; break; // R trigger - default: - ASSERT(FALSE); - } - } - - return dwPassword; -} - int CConfig::CheckParentPassword(const char *szCheckPassword) { (void)szCheckPassword; @@ -449,25 +420,15 @@ int CConfig::GetLaunchParameter2() return g_bHasLaunchData ? g_dwLaunchParameter2 : 0; } +// CConfig::CanDriveBeCleanup(drive) [script fn 'CanDriveBeCleanup', FND 0x0017fef8, sig 3] +// Reconstructed from 5960 retail XBE @ 0x0003b460: true for the drives a +// game may ask the dash to clean up, any case. 5960 takes F to M, T and U; +// Theseus has always taken H to O, T and U, which keeps the big F and G +// partitions out of it. int CConfig::CanDriveBeCleanup(int Drive) { - switch (toupper(Drive)) - { - case 'T': - case 'U': - case 'H': - case 'I': - case 'J': - case 'K': - case 'L': - case 'M': - case 'N': - case 'O': - return true; - - default: - return false; - } + int letter = toupper(Drive); + return letter == 'T' || letter == 'U' || (letter >= 'H' && letter <= 'O'); } DWORD CConfig::GetTitleID() @@ -531,15 +492,8 @@ int CConfig::NtFileExists(const char* FileName) // Translate Xbox path to local filesystem { - const char* translated = XboxFS_TranslatePath(szFile); char szLocal[MAX_PATH]; - if (translated) { - strncpy(szLocal, translated, MAX_PATH - 1); - szLocal[MAX_PATH - 1] = 0; - } else { - strncpy(szLocal, szFile, MAX_PATH - 1); - szLocal[MAX_PATH - 1] = 0; - } + XboxPath_Translate(szFile, szLocal, sizeof(szLocal)); for (char* p = szLocal; *p; p++) { if (*p == '\\') *p = '/'; } int result = (access(szLocal, F_OK) == 0) ? 1 : 0; diff --git a/theseus/desktop/d3d8_sdl.h b/theseus/desktop/d3d8_sdl.h index 452dae9..6d9dfd3 100644 --- a/theseus/desktop/d3d8_sdl.h +++ b/theseus/desktop/d3d8_sdl.h @@ -10,20 +10,12 @@ extern "C" unsigned char* stbi_load_from_memory(const unsigned char*, int, int*, int*, int*, int); extern "C" void stbi_image_free(void*); -// GL headers, pulled only for base typedefs (GLuint / GLint) that a few -// inert leftover state structs below still reference. No GL is called under -// bgfx; on Windows supplies the typedefs (GLEW is not needed). -#ifdef __APPLE__ - #define GL_SILENCE_DEPRECATION - #include -#elif defined(_WIN32) - #include - #include -#else - #define GL_GLEXT_PROTOTYPES - #include - #include -#endif +// A few inert leftover state structs below still hold GL-typed fields. +// No GL is called under bgfx, so the base typedefs are all they need. +typedef unsigned int GLuint; +typedef int GLint; +typedef unsigned int GLenum; +typedef unsigned short GLushort; // Ensure D3D vertex constructors are available (vertex8.h, hud.cpp) #ifndef DIRECT3D_VERSION @@ -388,8 +380,10 @@ struct D3DXCOLOR { #define TEXT(x) _T(x) #endif -// _stscanf +// _stscanf. mingw's tchar.h already maps it to sscanf in an ANSI build. +#ifndef _stscanf #define _stscanf sscanf +#endif // D3DX mesh stubs #define D3DXMESH_MANAGED D3DPOOL_MANAGED @@ -1651,13 +1645,6 @@ class IDirect3DDevice8 { if (m_tss0MagFilter == D3DTEXF_POINT) f |= BGFX_SAMPLER_MAG_POINT; return f; } - static GLenum D3DFilterToGL(DWORD d3dFilter, bool isMag) { - switch (d3dFilter) { - case 1: return GL_NEAREST; // D3DTEXF_POINT - case 2: return GL_LINEAR; // D3DTEXF_LINEAR - default: return isMag ? GL_LINEAR : GL_LINEAR; // ANISOTROPIC/QUINCUNX/GAUSSIAN → linear - } - } HRESULT SetTextureStageState(DWORD stage, D3DTEXTURESTAGESTATETYPE type, DWORD value) { // Sweep 2 trace counter. if (stage < 8 && (DWORD)type < 32) { @@ -2382,26 +2369,14 @@ inline HRESULT D3DXCreateMatrixStack(DWORD flags, ID3DXMatrixStack** pp) { *pp = #define D3DX_DEFAULT_NONPOW2 ((UINT)-2) #define D3DX_FILTER_NONE 1 -// Forward-declare XBX parser (defined in xbx_texture.h, included by Image.cpp/xip.cpp) +// Forward-declare XBX parser (defined in xbx_texture.h, included by asset_loader.cpp/xip_archive.cpp) IDirect3DTexture8* XBX_ParseTexture(const BYTE* pbContent, int cbContent); inline HRESULT D3DXCreateTextureFromFileA(IDirect3DDevice8* dev, const char* path, IDirect3DTexture8** ppTex) { (void)dev; *ppTex = NULL; - // Try to load the file from the xboxfs-mapped path + // Host path only. Xbox paths go through TheseusFOpen first. FILE* f = fopen(path, "rb"); - if (!f) { - // Try drive remap (Q:\path -> Data/path, etc.). - char mapped[1024]; - if (path[0] && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) { - const char* prefix = XboxFS_DriveToPrefix(path[0]); - if (prefix) { - snprintf(mapped, sizeof(mapped), "%s/%s", prefix, path + 3); - for (char* p = mapped; *p; p++) if (*p == '\\') *p = '/'; - f = fopen(mapped, "rb"); - } - } - } if (!f) return E_FAIL; fseek(f, 0, SEEK_END); long size = ftell(f); @@ -2481,11 +2456,14 @@ inline HRESULT D3DXCreateMeshFVF(DWORD numFaces, DWORD numVerts, DWORD opts, DWO } // ID3DXMesh::DrawSubset - renders mesh through the device +extern bool theseus_desktop_vs_valid; inline HRESULT ID3DXMesh::DrawSubset(DWORD attr) { (void)attr; if (!m_dev || !m_vbData || !m_ibData || m_numFaces == 0 || m_numVerts == 0) return S_OK; + // Sets the FVF behind TheseusSetVertexShader's back, so its cache can't be trusted after. m_dev->SetVertexShader(m_fvf); + theseus_desktop_vs_valid = false; m_dev->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, m_numVerts, m_numFaces, m_ibData, D3DFMT_INDEX16, m_vbData, m_vertStride); return S_OK; diff --git a/theseus/desktop/dashapp.cpp b/theseus/desktop/dashapp.cpp index dd782d1..beb3200 100755 --- a/theseus/desktop/dashapp.cpp +++ b/theseus/desktop/dashapp.cpp @@ -6,7 +6,7 @@ #include "std.h" #include "dashapp.h" #include "file_util.h" -#include "xip.h" +#include "xip_archive.h" #include "node.h" #include "screen.h" #include "runner.h" @@ -104,8 +104,6 @@ DWORD g_dwMainThreadId = 0; // Desktop-only state (was in CDashApp class, now standalone globals). int g_nVertPerFrame = 0; int g_nTriPerFrame = 0; -int g_nodeVisitsThisFrame = 0; -int g_nodeSkipsThisFrame = 0; char* g_szAppTitle = NULL; D3DXMATRIX g_matView; @@ -495,6 +493,7 @@ struct COLORVERTEX void Draw() { + Frame_PhaseBegin(FP_SCENE_SETUP); // Setup projection transform if (g_bProjectionDirty) { @@ -604,8 +603,8 @@ void Draw() TheseusSetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); TheseusSetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); - VERIFYHR(TheseusGetD3DDev()->SetStreamSource(0, m_pVB, sizeof(COLORVERTEX))); - VERIFYHR(TheseusGetD3DDev()->SetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE)); + TheseusSetStreamSource(0, m_pVB, sizeof(COLORVERTEX)); + TheseusSetVertexShader(D3DFVF_XYZ | D3DFVF_DIFFUSE); VERIFYHR(TheseusGetD3DDev()->DrawPrimitive(D3DPT_TRIANGLESTRIP, 0, 2)); TheseusSetTransform(D3DTS_PROJECTION, &matProjectionSave); @@ -714,6 +713,8 @@ void Draw() } // Draw the world + Frame_PhaseEnd(FP_SCENE_SETUP); + Frame_PhaseBegin(FP_SCENE_TREE); { // Clear active-keyboard tracker before scene render; visible CKeyboards // will re-set it via their Render() override. Hidden ones never run, so @@ -727,8 +728,11 @@ void Draw() g_pObject->Render(); TheseusPopWorld(); } + Frame_PhaseEnd(FP_SCENE_TREE); + Frame_PhaseBegin(FP_SCENE_END); TheseusEndScene(); + Frame_PhaseEnd(FP_SCENE_END); TheseusSetRenderState(D3DRS_SWATHWIDTH, D3DSWATH_OFF); TheseusPresent(); @@ -822,7 +826,10 @@ void CleanupApp() void Advance() { - XTIME now = (float)GetTickCount() / 1000.0f; + // Seconds since launch, on every host. Scripts time things like the + // screensaver against 0, so an uptime base fires them on the first frame. + static const Uint64 s_clockStart = SDL_GetPerformanceCounter(); + XTIME now = (double)(SDL_GetPerformanceCounter() - s_clockStart) / (double)SDL_GetPerformanceFrequency(); float nDelta = (float)(now - g_now); if (nDelta == 0.0f) nDelta = 0.001f; diff --git a/theseus/desktop/dashapp.h b/theseus/desktop/dashapp.h index 3f7d381..20fcae1 100755 --- a/theseus/desktop/dashapp.h +++ b/theseus/desktop/dashapp.h @@ -42,7 +42,6 @@ extern char* g_szAppTitle; // Desktop-only free functions int TheseusMessageBox(const char* szText, unsigned int uType = MB_OK); -void MakeAbsoluteURL(char* szBuf, const char* szBase, const char* szURL); void MakeAbsoluteURL(char* szBuf, const char* szURL); void UpdateCurDirFromFile(const char* szURL); extern int __cdecl NewFailed(size_t nBytes); diff --git a/theseus/desktop/dashinit.cpp b/theseus/desktop/dashinit.cpp index 2be5437..34fdc6a 100755 --- a/theseus/desktop/dashinit.cpp +++ b/theseus/desktop/dashinit.cpp @@ -5,7 +5,7 @@ #include "std.h" #include "dashapp.h" -#include "dashlocale.h" +#include "locale_node.h" #include "settingsfile.h" #include "network.h" #include "discord.h" @@ -130,9 +130,9 @@ void DashInit() { // Desktop: drives are mounted via xboxfs path mapping; no symlinks needed. // Ensure music directory exists. - CreateDirectory("e:\\TDATA", NULL); - CreateDirectory("e:\\TDATA\\fffe0000", NULL); - CreateDirectory("e:\\TDATA\\fffe0000\\music", NULL); + TheseusCreateDirectory("e:\\TDATA", NULL); + TheseusCreateDirectory("e:\\TDATA\\fffe0000", NULL); + TheseusCreateDirectory("e:\\TDATA\\fffe0000\\music", NULL); // Set current region g_nCurRegion = 1; // NA diff --git a/theseus/desktop/dashlocale.h b/theseus/desktop/dashlocale.h deleted file mode 100755 index fec2ed2..0000000 --- a/theseus/desktop/dashlocale.h +++ /dev/null @@ -1,55 +0,0 @@ -// dashlocale.h: dashboard locale ids. Region / language / video-mode -// enums and the LOCALE_FROM_REGION_LANGUAGE pack helper. - -#pragma once - -#define LOCALE_FROM_REGION_LANGUAGE(region, language) (((region) << 16) | (language)) -#define LANGUAGE_FROM_LOCALE(locale) ((locale) & 0x0000ffff) -#define REGION_FROM_LOCALE(locale) (((locale) >> 16) & 0x0000ffff) - -#define REGION_AMERICA 0 -#define REGION_EUROPE 1 -#define REGION_ASIA 2 - -#define MAX_TRANSLATE_LEN 1024 -#define MAX_LANGUAGE_CODE_LEN 3 - -#define LANGUAGE_CURRENT -1 -#define LANGUAGE_ENGLISH 0 -#define LANGUAGE_JAPANESE 1 -#define LANGUAGE_GERMAN 2 -#define LANGUAGE_FRENCH 3 -#define LANGUAGE_SPANISH 4 -#define LANGUAGE_ITALIAN 5 - -#define LANGUAGE_COUNT 6 - -#define LOCALE_CURRENT -1 -#define LOCALE_NA_ENGLISH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_NA, LANGUAGE_ENGLISH) -#define LOCALE_NA_JAPANESE LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_NA, LANGUAGE_JAPANESE) -#define LOCALE_NA_GERMAN LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_NA, LANGUAGE_GERMAN) -#define LOCALE_NA_FRENCH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_NA, LANGUAGE_FRENCH) -#define LOCALE_NA_SPANISH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_NA, LANGUAGE_SPANISH) -#define LOCALE_NA_ITALIAN LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_NA, LANGUAGE_ITALIAN) -#define LOCALE_JAPAN_ENGLISH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_JAPAN, LANGUAGE_ENGLISH) -#define LOCALE_JAPAN_JAPANESE LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_JAPAN, LANGUAGE_JAPANESE) -#define LOCALE_JAPAN_GERMAN LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_JAPAN, LANGUAGE_GERMAN) -#define LOCALE_JAPAN_FRENCH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_JAPAN, LANGUAGE_FRENCH) -#define LOCALE_JAPAN_SPANISH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_JAPAN, LANGUAGE_SPANISH) -#define LOCALE_JAPAN_ITALIAN LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_JAPAN, LANGUAGE_ITALIAN) -#define LOCALE_RESTOFWORLD_ENGLISH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_RESTOFWORLD, LANGUAGE_ENGLISH) -#define LOCALE_RESTOFWORLD_JAPANESE LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_RESTOFWORLD, LANGUAGE_JAPANESE) -#define LOCALE_RESTOFWORLD_GERMAN LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_RESTOFWORLD, LANGUAGE_GERMAN) -#define LOCALE_RESTOFWORLD_FRENCH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_RESTOFWORLD, LANGUAGE_FRENCH) -#define LOCALE_RESTOFWORLD_SPANISH LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_RESTOFWORLD, LANGUAGE_SPANISH) -#define LOCALE_RESTOFWORLD_ITALIAN LOCALE_FROM_REGION_LANGUAGE(XC_GAME_REGION_RESTOFWORLD, LANGUAGE_ITALIAN) - -extern void FormatInteger(char* szBuf, int nNumber, int locale = LOCALE_CURRENT); -extern void FormatBlocks(char* szBuf, int nNumber, int locale = LOCALE_CURRENT); -extern bool FormatTime(char* szBuf, SIZE_T chLen, SYSTEMTIME* pst, int locale = LOCALE_CURRENT); -extern const char* Translate(const char* szString, char* szTranslate, int nLanguage = LANGUAGE_CURRENT); - -extern int g_nCurLanguage; -extern int g_nCurRegion; - -const char* GetLanguageCode(char* sz); diff --git a/theseus/desktop/debug.cpp b/theseus/desktop/debug.cpp index baa590d..cfab81d 100755 --- a/theseus/desktop/debug.cpp +++ b/theseus/desktop/debug.cpp @@ -1,27 +1,22 @@ -// debug.cpp: desktop debug helpers. Implements TRACE / ASSERT and -// the LogComError / LogError pair declared in shared/theseus.h. -// Counterpart to engine/debug.cpp. +// debug.cpp: the desktop's logging backend. Trace prints to stderr; the +// HRESULT helpers format an error for the log. Counterpart to +// engine/debug.cpp. #include "std.h" #include "dashapp.h" #include "node.h" -// Trace is always available (TRACE macro compiles it out in release via dead-code elimination) +// Prints like printf to stderr. A leading \001 (the Xbox's severity mark) +// is dropped; messages past 511 characters are cut. extern "C" void Trace(const char* szMsg, ...) { + char text[512]; va_list args; va_start(args, szMsg); - - char szBuffer [512]; - vsnprintf(szBuffer, sizeof(szBuffer), szMsg, args); - - // Skip leading \001 control character (Xbox debug severity marker) - const char* szOut = szBuffer; - if (szOut[0] == '\001') szOut++; - - fprintf(stderr, "%s", szOut); - + vsnprintf(text, sizeof(text), szMsg, args); va_end(args); + + fputs(text[0] == '\001' ? text + 1 : text, stderr); } void TheseusGetErrorString(HRESULT hr, char* szErrorBuf, int cchErrorBuf) @@ -32,16 +27,14 @@ void TheseusGetErrorString(HRESULT hr, char* szErrorBuf, int cchErrorBuf) const char* TheseusGetErrorString(HRESULT hr) { - static char szBuf [100]; - TheseusGetErrorString(hr, szBuf, countof(szBuf)); - return szBuf; + static char text[100]; + TheseusGetErrorString(hr, text, sizeof(text)); + return text; } void LogComError(HRESULT hr, const char* szFunc/*= NULL*/) { - char szError [100]; - TheseusGetErrorString(hr, szError, countof(szError)); - Trace("\001Error in function: %s\n\001%s\n", szFunc == NULL ? "unknown" : szFunc, szError); + Trace("\001Error in function: %s\n\001%s\n", szFunc != NULL ? szFunc : "unknown", TheseusGetErrorString(hr)); } void LogError(const char* szFunc) @@ -49,21 +42,6 @@ void LogError(const char* szFunc) Trace("\001Error in function: %s\n", szFunc); } -extern "C" bool AssertFailed(const char* szFile, int nLine, HRESULT hr) -{ - if (hr == 0) - { - fprintf(stderr, "[ASSERT] %s:%d (last error 0x%x)\n", szFile, nLine, (unsigned)GetLastError()); - } - else - { - char szComError [100]; - TheseusGetErrorString(hr, szComError, countof(szComError)); - fprintf(stderr, "[ASSERT] %s:%d: %s\n", szFile, nLine, szComError); - } - return false; -} - void Debug_Init() { } @@ -72,53 +50,6 @@ void Debug_Exit() { } -void DumpHex(const uint8_t* pbData, int cbData, int cbMax/*=0*/) -{ - char szBuf [256]; - char szBuf2 [32]; - - bool bTruncated = false; - if (cbMax != 0 && cbData > cbMax) - { - bTruncated = true; - cbData = cbMax; - } - - char* pch = szBuf; - char* pch2 = szBuf2; - for (int i = 0; i < cbData; i += 1) - { - uint8_t b = *pbData++; - - pch += sprintf(pch, "%02x ", b); - - if (b >= ' ' && b < 128) - *pch2++ = b; - else - *pch2++ = '.'; - - if ((i & 15) == 15 || i == cbData - 1) - { - *pch2 = '\0'; - Trace("%-48s %s\n", szBuf, szBuf2); - pch = szBuf; - pch2 = szBuf2; - } - } - - if (bTruncated) - Trace("...\n"); -} - void Debug_Frame() { } - -void DumpRegisteredClasses() -{ - TRACE("Registered Classes:\n"); - for (CNodeClass* pClass = CNodeClass::c_pFirstClass; pClass != NULL; pClass = pClass->m_nextClass) - { - TRACE("\t%s\n", pClass->m_className); - } -} diff --git a/theseus/desktop/desktop_nodes.cpp b/theseus/desktop/desktop_nodes.cpp index 3b1e037..eba0dde 100644 --- a/theseus/desktop/desktop_nodes.cpp +++ b/theseus/desktop/desktop_nodes.cpp @@ -6,14 +6,7 @@ #include "app_paths.h" #include "std.h" -#ifdef _WIN32 -#include -#ifndef F_OK -#define F_OK 0 -#endif -// Windows wingdi.h defines GetFreeSpace as a macro; conflicts with our method -#undef GetFreeSpace -#endif +#include "host.h" #include "dashapp.h" #include "node.h" #include "runner.h" @@ -36,8 +29,8 @@ static const char* ResolveAudioURL(const TCHAR* url) // Check if it already has a drive letter (Q:\...) if (url[0] && url[1] == ':') { // Use XboxFS translation - const char* translated = XboxFS_TranslatePath(url); - return translated; + XboxPath_Translate(url, s_path, sizeof(s_path)); + return s_path; } // Relative path; prepend Data/ @@ -1208,7 +1201,7 @@ class CDiscDrive : public CNode public: static CDiscDrive* s_instance; - CDiscDrive() : m_discType(NULL), m_locked(false), m_pollTimer(2.0f) { + CDiscDrive() : m_discType(NULL), m_locked(false), m_bDeferNotification(false), m_pollTimer(2.0f) { m_discType = strdup("none"); s_instance = this; } @@ -1218,10 +1211,22 @@ class CDiscDrive : public CNode DECLARE_NODE_FUNCTIONS() TCHAR* m_discType; bool m_locked; + bool m_bDeferNotification; float m_pollTimer; + // CDiscDrive::Advance, 5960 @ 0x00039f20: a change seen while locked sets + // bDeferNotification; once unlocked the next frame clears it and fires + // OnDiscInserted, or OnDiscRemoved when the drive is empty. + void FireDeferred() { + if (!m_bDeferNotification || m_locked) return; + m_bDeferNotification = false; + bool isNone = !m_discType || strcmp(m_discType, "none") == 0; + CallFunction(this, isNone ? "OnDiscRemoved" : "OnDiscInserted"); + } + void Advance(float nSeconds) { CNode::Advance(nSeconds); + FireDeferred(); m_pollTimer += nSeconds; if (m_pollTimer < 2.0f) return; m_pollTimer = 0.0f; @@ -1247,6 +1252,8 @@ class CDiscDrive : public CNode CallFunction(this, "OnDiscInserted"); else if (isNone && !wasNone) CallFunction(this, "OnDiscRemoved"); + } else if (isNone != wasNone) { + m_bDeferNotification = true; } } @@ -1315,13 +1322,17 @@ void DiscDrive_SetDiscType(const char* type) { // Audio CD (or anything else): let the XAP script decide via OnDiscInserted if (!dd->m_locked) CallFunction(dd, "OnDiscInserted"); + else + dd->m_bDeferNotification = true; fprintf(stderr, "[DiscDrive] discType='%s', fired OnDiscInserted\n", type); } } +// 5960 PRD @ 0x00174550: discType, locked, bDeferNotification. START_NODE_PROPS(CDiscDrive, CNode) NODE_PROP(pt_string, CDiscDrive, discType) NODE_PROP(pt_boolean, CDiscDrive, locked) + NODE_PROP(pt_boolean, CDiscDrive, bDeferNotification) END_NODE_PROPS() #undef _FND_CLASS @@ -1346,13 +1357,6 @@ END_NODE_FUN() #include "media_player.h" #include "panel_shared.h" // for DashAudio_MuteAll/UnmuteAll #include -#ifdef _WIN32 -#include -#include -#else -#include -#include -#endif #define XBOX_BLOCK_SIZE 16384 #define SAVED_GAME_MAX_TITLES 256 @@ -1389,7 +1393,7 @@ static bool TryOpenXboxHDD(); // forward declaration // Defined in sdl_main.cpp, loaded from desktop.ini extern char g_qcowPath[512]; -// Called from xboxfs.h CreateFileA fallback to read files from FATX +// Called from TheseusCreateFile (xbox_file.cpp) to read save data off FATX. FILE* XboxFS_FATXReadFile(const char* xboxPath) { if (!s_xboxHDD.IsOpen()) { if (!s_xboxHDDTried) TryOpenXboxHDD(); // forward decl below @@ -1684,7 +1688,7 @@ static bool ParseXboxMetaValue(const char* xboxPath, const char* key, char* outB outBuf[0] = 0; // Read file through xboxfs layer - HANDLE hFile = CreateFile(xboxPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); + HANDLE hFile = TheseusCreateFile(xboxPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return false; unsigned char data[8192]; DWORD bytesRead = 0; @@ -1756,32 +1760,16 @@ static bool ParseXboxMetaValue(const char* xboxPath, const char* key, char* outB static uint64_t GetDirectorySize(const char* dirPath) { uint64_t total = 0; -#ifdef _WIN32 - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", dirPath); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) return 0; - do { - if (fd.name[0] == '.') continue; - if (!(fd.attrib & _A_SUBDIR)) - total += fd.size; - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(dirPath); + HostDir* dir = Host_OpenDir(dirPath); if (!dir) return 0; - struct dirent* ent; - while ((ent = readdir(dir)) != NULL) { - if (ent->d_name[0] == '.') continue; - char fullPath[1024]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, ent->d_name); - struct stat st; - if (stat(fullPath, &st) == 0 && S_ISREG(st.st_mode)) - total += st.st_size; - } - closedir(dir); -#endif + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + if (name[0] == '.') continue; + if (!(info.attrs & HOST_ATTR_DIR)) + total += info.size; + } + Host_CloseDir(dir); return total; } @@ -1826,7 +1814,7 @@ static void EnumerateTitles() title.hasImage = true; } else { // Synthetic UDATA entries fall back to Configs/icons/. - // xboxfs.h's C: routing maps "C:\icons\..." -> "Configs/icons/...". + // xbox_path.h maps "C:\icons\..." to "Configs/icons/...". snprintf(hostProbe, sizeof(hostProbe), "%s", AppPathf("Configs/icons/%s.jpg", entName)); if (access(hostProbe, F_OK) == 0) { snprintf(title.imagePath, sizeof(title.imagePath), @@ -1844,100 +1832,50 @@ static void EnumerateTitles() title.saveCount = 0; title.totalSizeBytes = 0; // Enumerate saves within this title -#ifdef _WIN32 - char savSearchBuf[1024]; - snprintf(savSearchBuf, sizeof(savSearchBuf), "%s\\*", titlePath); - struct _finddata_t savFd; - intptr_t hSavFind = _findfirst(savSearchBuf, &savFd); - if (hSavFind != -1) { - do { - if (savFd.name[0] == '.') continue; - if (!(savFd.attrib & _A_SUBDIR)) continue; - if (title.saveCount >= SAVED_GAME_MAX_SAVES) break; - char savePath[1024]; - snprintf(savePath, sizeof(savePath), "%s/%s", titlePath, savFd.name); - struct stat saveSt; - if (stat(savePath, &saveSt) != 0) continue; - DesktopSavedGame& save = title.saves[title.saveCount]; - memset(&save, 0, sizeof(save)); - strncpy(save.folderName, savFd.name, sizeof(save.folderName) - 1); - char xboxSaveMetaPath[256]; - snprintf(xboxSaveMetaPath, sizeof(xboxSaveMetaPath), "E:\\UDATA\\%s\\%s\\SaveMeta.xbx", entName, savFd.name); - if (!ParseXboxMetaValue(xboxSaveMetaPath, "Name", save.saveName, sizeof(save.saveName))) - strncpy(save.saveName, savFd.name, sizeof(save.saveName) - 1); - save.sizeBytes = GetDirectorySize(savePath); - save.modTime = saveSt.st_mtime; - title.totalSizeBytes += save.sizeBytes; - title.saveCount++; - } while (_findnext(hSavFind, &savFd) == 0); - _findclose(hSavFind); - } -#else - DIR* titleDir = opendir(titlePath); + HostDir* titleDir = Host_OpenDir(titlePath); if (titleDir) { - struct dirent* saveEnt; - while ((saveEnt = readdir(titleDir)) != NULL && title.saveCount < SAVED_GAME_MAX_SAVES) { - if (saveEnt->d_name[0] == '.') continue; + char saveName[260]; + HostFileInfo saveInfo; + while (title.saveCount < SAVED_GAME_MAX_SAVES && + Host_ReadDir(titleDir, saveName, sizeof(saveName), &saveInfo)) { + if (saveName[0] == '.') continue; + if (!(saveInfo.attrs & HOST_ATTR_DIR)) continue; char savePath[1024]; - snprintf(savePath, sizeof(savePath), "%s/%s", titlePath, saveEnt->d_name); - struct stat saveSt; - if (stat(savePath, &saveSt) != 0 || !S_ISDIR(saveSt.st_mode)) continue; + snprintf(savePath, sizeof(savePath), "%s/%s", titlePath, saveName); DesktopSavedGame& save = title.saves[title.saveCount]; memset(&save, 0, sizeof(save)); - strncpy(save.folderName, saveEnt->d_name, sizeof(save.folderName) - 1); + strncpy(save.folderName, saveName, sizeof(save.folderName) - 1); char xboxSaveMetaPath[256]; - snprintf(xboxSaveMetaPath, sizeof(xboxSaveMetaPath), "E:\\UDATA\\%s\\%s\\SaveMeta.xbx", entName, saveEnt->d_name); + snprintf(xboxSaveMetaPath, sizeof(xboxSaveMetaPath), "E:\\UDATA\\%s\\%s\\SaveMeta.xbx", entName, saveName); if (!ParseXboxMetaValue(xboxSaveMetaPath, "Name", save.saveName, sizeof(save.saveName))) - strncpy(save.saveName, saveEnt->d_name, sizeof(save.saveName) - 1); + strncpy(save.saveName, saveName, sizeof(save.saveName) - 1); save.sizeBytes = GetDirectorySize(savePath); - save.modTime = saveSt.st_mtime; + save.modTime = (time_t)saveInfo.mtime; title.totalSizeBytes += save.sizeBytes; title.saveCount++; } - closedir(titleDir); + Host_CloseDir(titleDir); } -#endif title.totalSizeBytes += GetDirectorySize(titlePath); s_titleCount++; }; -#ifdef _WIN32 - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", udataPath); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { fprintf(stderr, "[SavedGameGrid] Cannot open %s\n", udataPath); return; } - do { ProcessTitleEntry(fd.name); } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(udataPath); + HostDir* dir = Host_OpenDir(udataPath); if (!dir) { fprintf(stderr, "[SavedGameGrid] Cannot open %s\n", udataPath); return; } - struct dirent* ent; - while ((ent = readdir(dir)) != NULL) ProcessTitleEntry(ent->d_name); - closedir(dir); -#endif + char entry[260]; + while (Host_ReadDir(dir, entry, sizeof(entry), NULL)) ProcessTitleEntry(entry); + Host_CloseDir(dir); // Sort alphabetically for (int i = 0; i < s_titleCount - 1; i++) for (int j = i + 1; j < s_titleCount; j++) if (strcasecmp(s_titles[i].titleName, s_titles[j].titleName) > 0) { DesktopTitle tmp = s_titles[i]; s_titles[i] = s_titles[j]; s_titles[j] = tmp; } -#ifdef _WIN32 - { - ULARGE_INTEGER freeBytesAvail, totalBytes; - if (GetDiskFreeSpaceExA(udataPath, &freeBytesAvail, &totalBytes, NULL)) { - s_totalBlocks = (int)(totalBytes.QuadPart / XBOX_BLOCK_SIZE); - s_freeBlocks = (int)(freeBytesAvail.QuadPart / XBOX_BLOCK_SIZE); - } else - { s_totalBlocks = 50000; s_freeBlocks = 50000; } - } -#else - struct statvfs vfs; - if (statvfs(udataPath, &vfs) == 0) { - s_totalBlocks = (int)((uint64_t)vfs.f_blocks * vfs.f_frsize / XBOX_BLOCK_SIZE); - s_freeBlocks = (int)((uint64_t)vfs.f_bavail * vfs.f_frsize / XBOX_BLOCK_SIZE); + uint64_t freeBytes = 0, totalBytes = 0; + if (Host_DiskSpace(udataPath, &freeBytes, &totalBytes)) { + s_totalBlocks = (int)(totalBytes / XBOX_BLOCK_SIZE); + s_freeBlocks = (int)(freeBytes / XBOX_BLOCK_SIZE); } else { s_totalBlocks = 50000; s_freeBlocks = 50000; } -#endif fprintf(stderr, "[SavedGameGrid] Enumerated %d titles from %s\n", s_titleCount, udataPath); } @@ -2000,7 +1938,7 @@ static void LoadDesktopIcons() if (stat(iconPath, &_ist) != 0) continue; } - // Store Xbox-style path for VGames icon redirect in xboxfs.h + // Store the Xbox path so xbox_path.h can redirect it to the cached icon. snprintf(s_iconEntries[s_iconCount].iconPath, MAX_PATH, "%s:\\%s\\%s\\icon.jpg", g_vgames.games[i].drive, @@ -2811,7 +2749,8 @@ END_NODE_FUN() class CLiveAccounts : public CNode { public: - CLiveAccounts() : m_nCurrentIndex(0), m_bLogon(false), m_fLogOnSuccess(false), m_fLogOnInProgress(false) {} + CLiveAccounts() : m_nCurrentIndex(0), m_bLogon(false), m_fLogOnSuccess(false), m_fLogOnInProgress(false), + m_currentAccount(-1), m_newAccountFromMU(-1) {} DECLARE_NODE(CLiveAccounts, CNode) DECLARE_NODE_PROPS() @@ -2820,6 +2759,11 @@ class CLiveAccounts : public CNode int m_nCurrentIndex; bool m_bLogon, m_fLogOnSuccess, m_fLogOnInProgress; + // 5960 PRD @ 0x00176234: currentAccount and newAccountFromMU, both -1 + // until a logon or an MU account shows up. Desktop has neither. + int m_currentAccount; + int m_newAccountFromMU; + int GetNumberOfAccounts() { return 0; } int GetNumAccountsOnHD() { return 0; } CStrObject* GetAccountName(int) { return new CStrObject; } @@ -2838,6 +2782,10 @@ class CLiveAccounts : public CNode CStrObject* GetFriendInvites() { return new CStrObject; } CStrObject* GetNumberOfFriendsOnline() { return new CStrObject(_T("0")); } void LaunchEntryPoint(int, int, const TCHAR*) {} + // 5960 @ 0x000698b0 / 0x000698e0: an index past the account list reads + // as no passcode and a passcode that matches. Desktop has no accounts. + int IsPasswordEnabled(int) { return 0; } + int VerifyPassword(int, const TCHAR*) { return 1; } }; IMPLEMENT_NODE("LiveAccounts", CLiveAccounts, CNode) @@ -2847,6 +2795,8 @@ START_NODE_PROPS(CLiveAccounts, CNode) NODE_PROP(pt_boolean, CLiveAccounts, fLogOnSuccess) NODE_PROP(pt_boolean, CLiveAccounts, fLogOnInProgress) NODE_PROP(pt_integer, CLiveAccounts, nCurrentIndex) + NODE_PROP(pt_integer, CLiveAccounts, currentAccount) + NODE_PROP(pt_integer, CLiveAccounts, newAccountFromMU) END_NODE_PROPS() #undef _FND_CLASS @@ -2870,6 +2820,8 @@ START_NODE_FUN(CLiveAccounts, CNode) NODE_FUN_SV(GetFriendInvites) NODE_FUN_SV(GetNumberOfFriendsOnline) NODE_FUN_VIIS(LaunchEntryPoint) + NODE_FUN_II(IsPasswordEnabled) + NODE_FUN_IIS(VerifyPassword) END_NODE_FUN() // ============================================================================ @@ -2887,7 +2839,9 @@ class CDVDPlayer : public CNode m_scanSlow(false), m_autoStop(false), m_bScanBackward(false), m_backScanAccum(0.0f), m_digitAccum(0), m_digitTimer(0.0f), m_zoomScale(1.0f), m_zoomX(0.0f), m_zoomY(0.0f), - m_osdTimer(0.0f), m_lastOsdSeconds(-1) {} + m_osdTimer(0.0f), m_lastOsdSeconds(-1), + m_navDomain(0), m_navTitle(0), m_navChapter(0), m_navAngle(0), m_navAngleCount(0), + m_navVideoWidth(0), m_navVideoHeight(0) {} DECLARE_NODE(CDVDPlayer, CNode) DECLARE_NODE_PROPS() @@ -2913,6 +2867,49 @@ class CDVDPlayer : public CNode float m_osdTimer; // throttle OnTimeChange int m_lastOsdSeconds; // detect second change + // What the script was last told, so a change fires once. + int m_navDomain, m_navTitle, m_navChapter, m_navAngle, m_navAngleCount; + int m_navVideoWidth, m_navVideoHeight; + + // CDVDPlayer::Advance, 5960 @ 0x00065f10 (the part past the navigator + // calls, 0x00066029..0x0006639b), polls the navigator every frame and + // fires an event for each value that moved, in this order: + // domain -> OnDomainChange, title -> OnTitleChange, + // chapter -> OnChapterChange, h:m:s -> OnTimeChange, + // angle or angle count -> OnAngleChange, audio -> OnAudioChange, + // subtitle -> OnSubTitleChange, source video size -> OnVideoSizeChange + // Desktop already fires the time, audio and subtitle ones; these cover + // the rest from what mpv reports. + void ReportNavigationChanges() { + if (m_domain != m_navDomain) { + m_navDomain = m_domain; + CallFunction(this, "OnDomainChange"); + } + if (m_title != m_navTitle) { + m_navTitle = m_title; + CallFunction(this, "OnTitleChange"); + } + if (m_chapter != m_navChapter) { + m_navChapter = m_chapter; + CallFunction(this, "OnChapterChange"); + } + } + + void ReportAngleAndVideoSize() { + if (m_angle != m_navAngle || m_angleCount != m_navAngleCount) { + m_navAngle = m_angle; + m_navAngleCount = m_angleCount; + CallFunction(this, "OnAngleChange"); + } + int w = 0, h = 0; + MediaPlayer_GetVideoTexture(&w, &h); + if (w != m_navVideoWidth || h != m_navVideoHeight) { + m_navVideoWidth = w; + m_navVideoHeight = h; + CallFunction(this, "OnVideoSizeChange"); + } + } + // Update time + track properties from media player void UpdateFromMediaPlayer() { double pos = MediaPlayer_GetPosition(); @@ -2989,12 +2986,16 @@ class CDVDPlayer : public CNode // Ensure we stay visible while playing if (!m_visible) m_visible = true; + ReportNavigationChanges(); + // Fire time change callback only when the displayed second changes int curSec = m_hours * 3600 + m_minutes * 60 + m_seconds; if (curSec != m_lastOsdSeconds) { m_lastOsdSeconds = curSec; CallFunction(this, "OnTimeChange"); } + + ReportAngleAndVideoSize(); } else if (mpState == MP_STOPPED && m_playbackMode != 0) { m_playbackMode = 0; m_scanSpeed = 1; @@ -3003,6 +3004,7 @@ class CDVDPlayer : public CNode m_title = 0; m_domain = 0; CallFunction(this, "OnPlaybackModeChange"); + ReportNavigationChanges(); } // Sync A-B repeat state @@ -3018,6 +3020,16 @@ class CDVDPlayer : public CNode MediaPlayer_RenderToScreen(0, 0); } + // 5960 ends each user op on the navigator's answer (helper @ 0x000669f0 + // and its inlined copies through 0x00067f00): + // hr == 0x87840001 (UOP prohibited) or 0x80034013 -> OnProhibited + // anything else -> OnUserOpAllowed + // mpv takes every transport op; the disc menu, select and angle ops have + // nothing to act on in a file, so they report prohibited. + void ReportUserOp(bool bAllowed) { + CallFunction(this, bAllowed ? "OnUserOpAllowed" : "OnProhibited"); + } + // === Transport === void init() {} @@ -3031,6 +3043,7 @@ class CDVDPlayer : public CNode } MediaPlayer_Play(); m_playbackMode = 3; // DPM_PLAYING + ReportUserOp(true); } void stop() { @@ -3046,7 +3059,7 @@ class CDVDPlayer : public CNode MediaPlayer_Stop(); } - void pause() { MediaPlayer_Pause(); } + void pause() { MediaPlayer_Pause(); ReportUserOp(true); } void playOrPause() { // If scanning, stop scan and resume normal playback if (m_playbackMode == 5 || m_playbackMode == 8 || m_scanSpeed > 1 || m_bScanBackward) { @@ -3057,11 +3070,13 @@ class CDVDPlayer : public CNode MediaPlayer_SetSpeed(1.0); MediaPlayer_Play(); m_playbackMode = 3; // DPM_PLAYING + ReportUserOp(true); return; } MediaPlayer_TogglePause(); + ReportUserOp(true); } - void resume() { MediaPlayer_Play(); } + void resume() { MediaPlayer_Play(); ReportUserOp(true); } // === Scanning (fast forward/rewind with speed ramp) === void forwardScan() { @@ -3087,6 +3102,7 @@ class CDVDPlayer : public CNode MediaPlayer_SetSpeed((double)m_scanSpeed); } m_playbackMode = 5; // DPM_SCANNING + ReportUserOp(true); } void backwardScan() { @@ -3114,6 +3130,7 @@ class CDVDPlayer : public CNode } m_backScanAccum = 0.0f; m_playbackMode = 5; // DPM_SCANNING + ReportUserOp(true); } void stopScan() { @@ -3124,48 +3141,53 @@ class CDVDPlayer : public CNode MediaPlayer_SetSpeed(1.0); MediaPlayer_Play(); m_playbackMode = 3; // DPM_PLAYING + ReportUserOp(true); } // === Frame stepping === void frameAdvance() { MediaPlayer_FrameStep(); m_playbackMode = 8; // DPM_TRICKPLAY + ReportUserOp(true); } void frameReverse() { MediaPlayer_FrameBackStep(); m_playbackMode = 8; // DPM_TRICKPLAY + ReportUserOp(true); } // === Chapter navigation === - void startChapter() { MediaPlayer_Seek(0); } - void nextChapter() { MediaPlayer_NextChapter(); } - void prevChapter() { MediaPlayer_PrevChapter(); } + void startChapter() { MediaPlayer_Seek(0); ReportUserOp(true); } + void nextChapter() { MediaPlayer_NextChapter(); ReportUserOp(true); } + void prevChapter() { MediaPlayer_PrevChapter(); ReportUserOp(true); } // === Track selection === void nextAudioStream() { MediaPlayer_NextAudioTrack(); UpdateTrackLanguages(); // refresh language strings after track change CallFunction(this, "OnAudioChange"); + ReportUserOp(true); } void nextSubtitle() { MediaPlayer_NextSubtitleTrack(); UpdateTrackLanguages(); // refresh language strings after track change CallFunction(this, "OnSubTitleChange"); + ReportUserOp(true); } - void nextAngle() {} // N/A for file playback + void nextAngle() { ReportUserOp(false); } // N/A for file playback - // === DVD menu navigation (no-ops for file playback) === - void selectUp() {} - void selectDown() {} - void selectRight() {} - void selectLeft() {} - void activate() {} - void goUp() {} - void menu() {} - void titleMenu() {} + // === DVD menu navigation (nothing to navigate in a file) === + void selectUp() { ReportUserOp(false); } + void selectDown() { ReportUserOp(false); } + void selectRight() { ReportUserOp(false); } + void selectLeft() { ReportUserOp(false); } + void activate() { ReportUserOp(false); } + void goUp() { ReportUserOp(false); } + void menu() { ReportUserOp(false); } + void titleMenu() { ReportUserOp(false); } // === Zoom === void setScale(float scale) { @@ -3192,6 +3214,7 @@ class CDVDPlayer : public CNode MediaPlayer_ClearABLoop(); m_abRepeatState = 0; } + ReportUserOp(true); } // === Digit entry (chapter jump) === @@ -3337,10 +3360,8 @@ class CPlexLibrary : public CNode { if (!Plex_HasToken()) return; Plex_StartSync(); - if (m_serverUri.empty() && Plex_ServersReady()) { - std::vector srv = Plex_GetServers(); - if (!srv.empty()) m_serverUri = srv[0].uri; - } + // Playback goes to the server the sync settled on, which isn't always the first listed. + if (Plex_SyncReady()) m_serverUri = Plex_GetActiveServerUri(); } int GetCount() @@ -3575,6 +3596,7 @@ class CPlexLibrary : public CNode const std::string& title, const std::string& subtitle) { + if (m_serverUri.empty()) m_serverUri = Plex_GetActiveServerUri(); if (m_serverUri.empty() || ratingKey.empty()) return; std::string url = Plex_ResolveStreamUrl(m_serverUri, ratingKey); if (url.empty()) { @@ -4415,7 +4437,7 @@ class CGamesCollection : public CNode return new CStrObject(g_vgames.games[m_slice[t]].launch); } - // Returns the Xbox-style icon path. xboxfs.h intercepts the load and + // Returns the Xbox-style icon path. xbox_path.h intercepts the load and // redirects to Configs/icons/.{jpg,png} via VGames_GetIconPath. CStrObject* GetIcon(int i) { diff --git a/theseus/desktop/desktop_stubs.cpp b/theseus/desktop/desktop_stubs.cpp index 305cccf..04c13e0 100644 --- a/theseus/desktop/desktop_stubs.cpp +++ b/theseus/desktop/desktop_stubs.cpp @@ -8,6 +8,7 @@ #include "runner.h" #include "titlecollection.h" #include "discmanager.h" +#include "network.h" // g_titles - from TitleCollection.cpp (removed) CTitleArray g_titles[9]; @@ -108,7 +109,8 @@ int CXBExecutable::ReadFile(const char* szFileName, const bool, const bool) { strncpy(m_szFileName, szFileName, sizeof(m_szFileName) - 1); // Try to open the real file first - const char* translated = XboxFS_TranslatePath(szFileName); + char translated[512]; + XboxPath_Translate(szFileName, translated, sizeof(translated)); FILE* f = fopen(translated, "rb"); if (f) { // Real XBE file exists; read header for title info. @@ -138,3 +140,17 @@ int CXBExecutable::ReadFile(const char* szFileName, const bool, const bool) { return 0; } +// net:: from xbox/network.cpp. No FTP UI on desktop today. +namespace net +{ + bool init() { return true; } + void shutdown() {} + void restart() {} + bool isReady() { return false; } + bool isFtpRunning() { return false; } + DWORD getAddress() { return 0; } + DWORD getSubnet() { return 0; } + DWORD getGateway() { return 0; } + DWORD getPrimaryDns() { return 0; } + DWORD getSecondaryDns(){ return 0; } +} diff --git a/theseus/desktop/esde.cpp b/theseus/desktop/esde.cpp index e1bd2d3..8fc682a 100644 --- a/theseus/desktop/esde.cpp +++ b/theseus/desktop/esde.cpp @@ -6,13 +6,8 @@ #include #include #include -#ifdef _WIN32 -#include -#define strcasecmp _stricmp -#else -#include #include -#endif +#include "host.h" // Lives here so the ES-DE scanner stays linkable on its own. sdl_main points // it at the saved value. @@ -131,67 +126,80 @@ const char* Esde_RomRoot(const char* esdeRoot) return s_root; } +static bool g_esdeFoundSystemsXml = false; + // Custom systems win, same order ES-DE uses, then the copy inside the install. +// An AppImage keeps its copy inside the mount, so nothing is on disk for us to +// read; those users point us straight at an es_systems.xml instead. static bool FindSystemsXml(const char* esdeRoot, char* out, size_t outSz) { struct stat st; + + // Pointed straight at the file. + size_t rl = strlen(esdeRoot); + const size_t kName = sizeof("es_systems.xml") - 1; + if (rl >= kName && strcasecmp(esdeRoot + rl - kName, "es_systems.xml") == 0 && + stat(esdeRoot, &st) == 0) { + snprintf(out, outSz, "%s", esdeRoot); + return true; + } + snprintf(out, outSz, "%s/custom_systems/es_systems.xml", esdeRoot); + fprintf(stderr, "[ES-DE] probe %s\n", out); if (stat(out, &st) == 0) return true; - static const char* kBundled[] = { + char home[1024]; + snprintf(home, sizeof(home), "%s", PlatformHomeDir()); + + char userLocal[1024], homeEsde[1024]; + snprintf(userLocal, sizeof(userLocal), + "%s/.local/share/es-de/resources/systems/linux/es_systems.xml", home); + snprintf(homeEsde, sizeof(homeEsde), + "%s/ES-DE/resources/systems/linux/es_systems.xml", home); + + const char* kBundled[] = { "/Applications/ES-DE.app/Contents/Resources/resources/systems/macos/es_systems.xml", "/usr/share/es-de/resources/systems/linux/es_systems.xml", + "/usr/local/share/es-de/resources/systems/linux/es_systems.xml", + "/usr/share/emulationstation/resources/systems/linux/es_systems.xml", "/app/share/es-de/resources/systems/linux/es_systems.xml", "/var/lib/flatpak/app/org.es_de.frontend/current/active/files/share/es-de/resources/systems/linux/es_systems.xml", + "/var/lib/snapd/snap/es-de/current/usr/share/es-de/resources/systems/linux/es_systems.xml", + userLocal, + homeEsde, "C:/Program Files/ES-DE/resources/systems/windows/es_systems.xml", }; for (size_t i = 0; i < sizeof(kBundled) / sizeof(kBundled[0]); i++) { + fprintf(stderr, "[ES-DE] probe %s\n", kBundled[i]); if (stat(kBundled[i], &st) == 0) { snprintf(out, outSz, "%s", kBundled[i]); return true; } } + out[0] = '\0'; return false; } +// True when we could not find a systems definition at all, which is a +// different failure from finding one and matching no roms. +bool Esde_LastScanFoundSystemsXml(void) +{ + return g_esdeFoundSystemsXml; +} + // One directory level. cb gets (name, isDir) and stops the walk on false. template static void ForEachEntry(const char* dir, F cb) { -#ifdef _WIN32 - char pattern[1024]; - snprintf(pattern, sizeof(pattern), "%s/*", dir); - struct _finddata_t fd; - intptr_t h = _findfirst(pattern, &fd); - if (h == -1) return; - do { - if (fd.name[0] == '.') continue; - if (!cb(fd.name, (fd.attrib & _A_SUBDIR) != 0)) break; - } while (_findnext(h, &fd) == 0); - _findclose(h); -#else - DIR* d = opendir(dir); + HostDir* d = Host_OpenDir(dir); if (!d) return; - struct dirent* e; - while ((e = readdir(d)) != NULL) { - if (e->d_name[0] == '.') continue; - // readdir already knows the type. stat() per entry costs a network - // round trip, which on an SMB rom library is the whole scan. - bool isDir; - if (e->d_type == DT_DIR) isDir = true; - else if (e->d_type == DT_REG) isDir = false; - else { - // Symlinks and anything readdir won't commit to still need a look. - char full[1024]; - snprintf(full, sizeof(full), "%s/%s", dir, e->d_name); - struct stat st; - if (stat(full, &st) != 0) continue; - isDir = S_ISDIR(st.st_mode) != 0; - } - if (!cb(e->d_name, isDir)) break; + char name[256]; + bool isDir; + while (Host_ReadDirType(d, name, sizeof(name), &isDir)) { + if (name[0] == '.') continue; + if (!cb(name, isDir)) break; } - closedir(d); -#endif + Host_CloseDir(d); } // A .app is a directory on macOS, so extension wins over file type. Anything @@ -244,7 +252,12 @@ int Esde_ScanSystems(const char* esdeRoot, EsdeSystem* out, int maxOut) if (!esdeRoot || !*esdeRoot || !out || maxOut <= 0) return 0; char sysXml[700]; - if (!FindSystemsXml(esdeRoot, sysXml, sizeof(sysXml))) return 0; + g_esdeFoundSystemsXml = FindSystemsXml(esdeRoot, sysXml, sizeof(sysXml)); + if (!g_esdeFoundSystemsXml) { + fprintf(stderr, "[ES-DE] no es_systems.xml found for root %s\n", esdeRoot); + return 0; + } + fprintf(stderr, "[ES-DE] systems from %s\n", sysXml); FILE* fp = fopen(sysXml, "rb"); if (!fp) return 0; diff --git a/theseus/desktop/esde.h b/theseus/desktop/esde.h index d8e057a..d1ba5b1 100644 --- a/theseus/desktop/esde.h +++ b/theseus/desktop/esde.h @@ -26,6 +26,10 @@ struct EsdeGame { // Systems whose rom folder exists and holds at least one matching file. int Esde_ScanSystems(const char* esdeRoot, EsdeSystem* out, int maxOut); +// False when the last scan never located an es_systems.xml, which is a +// different failure from locating one and matching no roms. +bool Esde_LastScanFoundSystemsXml(void); + // Games for one system, subfolders included. Names come from gamelist.xml // where it has them, the filename otherwise. int Esde_ScanGames(const char* esdeRoot, const EsdeSystem* sys, EsdeGame* out, int maxOut); diff --git a/theseus/desktop/esde_vgames.cpp b/theseus/desktop/esde_vgames.cpp index 36cafc4..ec5bff4 100644 --- a/theseus/desktop/esde_vgames.cpp +++ b/theseus/desktop/esde_vgames.cpp @@ -65,7 +65,8 @@ static void ClearEsdeEntries() if (g.valid && g.transient && strcasecmp(g.category, ESDE_CATEGORY) == 0) g.valid = false; } - g_vgames.generation++; + // Every resync re-adds the whole library, so hand the old slots back. + VGames_Compact(); } void EsdeVGames_Clear() diff --git a/theseus/desktop/files.cpp b/theseus/desktop/files.cpp index 30d1951..b5ac74a 100755 --- a/theseus/desktop/files.cpp +++ b/theseus/desktop/files.cpp @@ -159,7 +159,7 @@ void CFolder::Refresh(const char* szPath) char szWildA [MAX_PATH]; Ansi(szWildA, szWild, MAX_PATH); - h = FindFirstFile(szWildA, &fd); + h = TheseusFindFirstFile(szWildA, &fd); if (h == INVALID_HANDLE_VALUE) { @@ -238,9 +238,9 @@ void CFolder::Refresh(const char* szPath) pFile->m_length = fd.nFileSizeLow; } } - while (FindNextFile(h, &fd)); + while (TheseusFindNextFile(h, &fd)); - FindClose(h); + TheseusFindClose(h); } static int __cdecl CmpName(const void* p1, const void* p2) diff --git a/theseus/desktop/hdd_browser.cpp b/theseus/desktop/hdd_browser.cpp index 096a16e..087ac50 100644 --- a/theseus/desktop/hdd_browser.cpp +++ b/theseus/desktop/hdd_browser.cpp @@ -16,26 +16,13 @@ #include #include -#ifndef _WIN32 -#include +#include "platform.h" +#include "host.h" #include +#include +#ifndef THESEUS_HOST_WIN #include #include -#else -#include -#include -#include -#include -#include -// windows.h #defines CreateFile/CreateDirectory as A/W macros, which -// collides with FATXReader::CreateFile / FATXReader::CreateDirectory. -// Drop the macros so the class methods compile under their real names. -#ifdef CreateFile -#undef CreateFile -#endif -#ifdef CreateDirectory -#undef CreateDirectory -#endif #endif bool g_hddBrowserOpen = false; @@ -104,15 +91,15 @@ static const char* GetDefaultQcow2Path() { static char path[512] = ""; if (path[0]) return path; -#ifdef __APPLE__ +#ifdef THESEUS_HOST_MAC const char* home = getenv("HOME"); if (home) snprintf(path, sizeof(path), "%s/Library/Application Support/xemu/xemu/xbox_hdd.qcow2", home); -#elif defined(__linux__) +#elif defined(THESEUS_HOST_LINUX) const char* home = getenv("HOME"); if (home) snprintf(path, sizeof(path), "%s/.local/share/xemu/xemu/xbox_hdd.qcow2", home); -#elif defined(_WIN32) +#elif defined(THESEUS_HOST_WIN) char* appdata = getenv("LOCALAPPDATA"); if (appdata) snprintf(path, sizeof(path), "%s\\xemu\\xemu\\xbox_hdd.qcow2", appdata); @@ -402,37 +389,18 @@ static int ImportFolderRecursive(FATXReader* part, uint32_t fatxDirCluster, } }; -#ifdef _WIN32 - char searchBuf[1024]; - snprintf(searchBuf, sizeof(searchBuf), "%s\\*", hostPath); - struct _finddata_t fd; - intptr_t hFind = _findfirst(searchBuf, &fd); - if (hFind == -1) { - fprintf(stderr, "[Import] _findfirst failed: '%s'\n", hostPath); - return 0; - } - do { - bool isDir = (fd.attrib & _A_SUBDIR) != 0; - bool isFile = !isDir; - ProcessEntry(fd.name, isDir, isFile); - } while (_findnext(hFind, &fd) == 0); - _findclose(hFind); -#else - DIR* dir = opendir(hostPath); + HostDir* dir = Host_OpenDir(hostPath); if (!dir) { - fprintf(stderr, "[Import] opendir failed: '%s' (errno %d)\n", hostPath, errno); + fprintf(stderr, "[Import] cannot open folder: '%s'\n", hostPath); return 0; } - struct dirent* ent; - while ((ent = readdir(dir)) != nullptr) { - char fullPath[1024]; - snprintf(fullPath, sizeof(fullPath), "%s/%s", hostPath, ent->d_name); - struct stat st; - if (stat(fullPath, &st) != 0) continue; - ProcessEntry(ent->d_name, S_ISDIR(st.st_mode), S_ISREG(st.st_mode)); + char name[260]; + HostFileInfo info; + while (Host_ReadDir(dir, name, sizeof(name), &info)) { + bool isDir = (info.attrs & HOST_ATTR_DIR) != 0; + ProcessEntry(name, isDir, !isDir); } - closedir(dir); -#endif + Host_CloseDir(dir); return imported; } diff --git a/theseus/desktop/host.h b/theseus/desktop/host.h new file mode 100644 index 0000000..83316d5 --- /dev/null +++ b/theseus/desktop/host.h @@ -0,0 +1,101 @@ +// host.h: the narrow door from the desktop build to the host OS. Plain +// types only, so it never drags an OS header into Xbox-shaped code. +// host_posix.cpp (mac, Linux) and host_win.cpp implement it. +// +// Every path here is a host path with '/' separators. Xbox paths go +// through xbox_path.h first. + +#pragma once + +#include +#include +#include + +enum { + HOST_ATTR_DIR = 0x01, + HOST_ATTR_HIDDEN = 0x02, + HOST_ATTR_SYSTEM = 0x04, + HOST_ATTR_READONLY = 0x08, +}; + +struct HostFileInfo { + uint32_t attrs; // HOST_ATTR_* + uint64_t size; + int64_t mtime; // seconds since 1970, UTC +}; + +// The separator native paths show the user. +extern const char Host_PathSep; + +// Files and folders. Host_MakeDir is true when the folder exists afterwards. +bool Host_MakeDir(const char* path); +bool Host_RemoveDir(const char* path); +bool Host_RemoveFile(const char* path); +bool Host_Stat(const char* path, HostFileInfo* out); +bool Host_StatOpenFile(FILE* f, HostFileInfo* out); +bool Host_SetAttrs(const char* path, uint32_t attrs); +bool Host_RenameReplace(const char* from, const char* to); +// Pushes a closed file's data all the way to disk, so a rename after it can't land first. +bool Host_FlushToDisk(const char* path); +bool Host_CopyFile(const char* from, const char* to, bool failIfExists); +bool Host_DiskSpace(const char* path, uint64_t* freeBytes, uint64_t* totalBytes); + +// An empty read/write file that's deleted when closed. +FILE* Host_TempFile(); + +// 64-bit seek on an open file. Returns the new position, or -1. +int64_t Host_Seek(FILE* f, int64_t offset, int whence); + +// Folder listing. Hands back every entry, "." and ".." included. +struct HostDir; +HostDir* Host_OpenDir(const char* path); +bool Host_ReadDir(HostDir* dir, char* name, size_t nameSize, HostFileInfo* info); +// Name and folder-or-not only. Skips the per-entry stat when the OS already +// knows the type, which matters on a network share. Entries that can't be +// looked at are skipped. +bool Host_ReadDirType(HostDir* dir, char* name, size_t nameSize, bool* isDir); +void Host_CloseDir(HostDir* dir); + +// The machine itself. +bool Host_MemoryStatus(uint64_t* totalBytes, uint64_t* availBytes); +uint32_t Host_LocalIPv4(uint32_t* netmask); // network order, 0 when there's none + +// Process plumbing for main() and the launcher. host_win_app.cpp and +// host_posix_app.cpp. +struct SDL_Window; + +// Windows only: attach to the parent shell's console, or with none (an +// Explorer launch) send stdout and stderr to logPath. No-op elsewhere. +void Host_ConsoleOrLog(const char* logPath); +// Ask the OS for game-like scheduling. No-op where there's nothing to ask. +void Host_BoostProcess(); +// Each crash writes crashes/