Skip to content

UIX Desktop 0.4.0 - #89

Merged
MrMilenko merged 193 commits into
mainfrom
release-0.4.0
Nov 12, 2026
Merged

MrMilenko merged 193 commits into
mainfrom
release-0.4.0

Conversation

@MrMilenko

Copy link
Copy Markdown
Owner

UIX Desktop 0.4.0, for the Xbox's 25th birthday.

What's in it

  • Rebuilt engine. The script compiler and runner, node system, scenes, materials, text, keyboard, XIP reader and the rest of what the desktop build ships are written from binary analysis of the 5960 retail dashboard. Text, keyboard, XIP, locale, materials and the image/texture/mesh loaders are one shared copy for both builds now.
  • Windows rework. Only the new host layer talks to Windows, so dashboard code can't fall through to Win32 with Xbox paths anymore. make lint-platform and make win64-import-guard keep it that way.
  • Updates reach existing installs. Shipped XIPs, fonts and languages used to be copied once and never refreshed. Untouched copies update now; anything you edited is kept.
  • Settings can't be lost to a crash mid-save, a log file on every OS (theseus.log), and crash reports in the crashes folder.
  • 5960 parity: Background fade/alpha, Text indents and dates, the Keyboard's TID functions and props, Live passcode checks, DVD player events, and more.
  • Docs: a new README built around the desktop app, plus updated build and porting docs, and issue forms.

Fixes

Fixes #83, fixes #85, fixes #86, fixes #37, fixes #71, fixes #44, fixes #42, fixes #24, fixes #58, fixes #79, fixes #80.

Also: Windows dashboard launches, desktop frame timing, garbled desktop keyboard text, sounds playing fast on 48 kHz and surround devices, random values out of range on mac/Linux, a mesh buffer leak on every skin switch, UIX Settings and Skins menu script errors, playlists losing items, and a pile of crashes on broken XIPs and scripts.

Probably fixed, needs a retest on this build: #77, #39, #18. Still open: #76, #38, #47, #41, #54, #84, #28.

Tested

mac desktop, Linux x64/ARM64 and Windows x64 builds in CI; Windows run under Wine; Xbox retail and debug compile. Not yet run on Xbox hardware.

pkg-config is the dependency contract now, with Homebrew keg-only paths
folded in when brew exists. README and Makefile list deps for apt, dnf,
pacman, brew, MacPorts and MSYS2. cxbe follows OXDK's move under xbox/
and fails loudly instead of silently producing nothing.
shared/platform.h is the first include everywhere. A build is _XBOX or
THESEUS_DESKTOP, and a desktop build is one of THESEUS_HOST_WIN/MAC/LINUX.
Bad combinations stop the compile. _WIN32 is true on the Xbox too, so
Xbox-built code no longer reads it.

Retires _DESKTOP, UIX_DESKTOP and _WINDOWS. make lint-platform keeps them
out and runs in CI.
Code holding an Xbox path calls the Theseus file functions in theseus.h:
CreateFile, CreateDirectory, DeleteFile, RemoveDirectory, the attribute
and free-space queries, FindFirst/Next/Close, MoveFile, CopyFile and
FOpen. The Xbox build passes each straight to XTL. The desktop build
translates first (xbox_file.cpp) and host paths call the OS directly.

xbox_path.h is the one translator, caller-owned buffers, with a write
variant that never follows the virtual-game icon redirect. xboxfs.h,
the fopen and Win32-name macros, the POSIX Win32 path stubs, the lite
translator and the Windows host-path whitelist are gone.

Fixes along the way: Windows created TDATA on a real E: drive,
TheseusCreateDirectory made literal Q:\ dirs on mac/Linux, lstrcpyn
didn't terminate on POSIX, CGameCopier::DeleteFile recursed on desktop,
and texture loads decode the bytes they fetched instead of reopening
by name. -Wno-macro-redefined is gone; the build is clean without it.
Configs/Library mkdirs used relative paths and dropped folders in the
working directory. The hang dump and graphics log now land in the user
data dir. MediaDB's cache rename replaces the old file on Windows,
where rename() won't.
shared/frame_profiler replaces the loose g_* counters and hand-rolled
averaging: fixed phases and counters, averaged over 250ms, read as one
snapshot by the perf widget, the fps widget, the System page and the
desktop --graphics-debug overlay.

Xbox measures CPU time blocked on the GPU with a per-frame fence, only
while the perf widget is on screen. Desktop pulls GPU time, draw count
and transient buffer use from bgfx::getStats, and --graphics-debug
writes a line every 5s to theseus_perf.log in the user data dir.

Scene draws are counted apart from the HUD, and D3DX text mesh draws,
world updates, state calls and repeats, disk queries and Inline load
time are counted for the first time. Xbox state repeats are counted
against a shadow copy only; every call still goes to the device.
Went through date, math, string, array, file_util, timezone and screen and
noted where the code has to match the dashboard: calendar math, the JS
built-ins, the script tables. Each note says what the binary does and where.
Moved every material the 5960 dash builds into materials_retail.inl, shared
by both builds. Stock.xbx doesn't have to list anything anymore, and other
skins only need the materials they change.
A lot of bounds checks were ASSERTs, which are gone in retail. Fixed the ones
that matter: XIP and XPR loading, the skin path from Config.ini,
FormatMemoryUnit on the hard drive slot, save metadata parsing, the script
stack and settings writes per key. The panic screen now says what to do
next. Booted and launched games on hardware once; not everything is covered.
ES-DE can be pointed straight at an es_systems.xml (AppImage installs keep
theirs inside the image), and the empty-scan message says which problem you
hit. Movies and episodes dropped loose in a folder show up now instead of
being skipped.
The Xbox now drops render and texture stage states that didn't change, and
forgets them on Reset and in the panic screen. Overlays set their full 2D
state themselves since they can't count on leftovers anymore. The profiler
splits the tree into material, world and draw and lists the heaviest meshes.
Needs a hardware test.
Index buffers go into their vertex buffer's slot, but the check only looked
at the index count. On desktop the check was an ASSERT that just warned and
kept writing. Both readers skip the extra entry now, and check the mesh
index before drawing.
It compared 15 characters against a 14 character name, so it never matched.
Plex, Jellyfin and xCloud read the in-flight map outside its lock while
finished downloads were erasing from it on other threads.
Advance() used GetTickCount in a float. On Windows that's system uptime in
15.6 ms steps, so animation got choppier the longer the PC had been up. It
uses the SDL performance counter in a double now, same as mac and Linux.
Still needs a run on Windows.
Every episode dropped straight in the TV root got the root folder as its show path, so they all collided when TMDB data carried over on a rescan. Each one now keys on its own file.
Episodes sitting straight in a show folder came out in whatever order the disk listed them. Now they sort by season and episode from the filename (S01E02, s1e2, 1x02), and anything unnumbered follows in natural name order.
Reloading games.ini wiped the injected xCloud and ES-DE rows, and they stayed gone until a restart. The reload now carries them over since they never live in the file.
A resync only marked the old ES-DE rows dead and appended the whole library again, so every rescan burned another library's worth of slots. Same with an xCloud refresh. Dead rows now get compacted away, and Title Maker drops its cached rows and selections when that happens.
Opening a show or season read the server list straight off while a sync worker could be swapping it out after a sign in. It copies the address under the lock now.
The lookup workers each save a checkpoint every 10 titles, all through the same temp file. Two at once could interleave and rename a scrambled cache into place. Saves take turns now.
The last change put the clock on the performance counter, which on mac
counts from boot. Scripts time the screensaver against zero, so it fired on
the first frame before the menu existed and threw an array error. Windows
had the same problem before, since GetTickCount is uptime there too.
ASSERT is nothing in retail and only a warning on desktop, and a lot of
code leaned on it anyway. Switched the ones that touch real memory: long
numbers in scripts overran a stack buffer in the compiler, big language
files ran off the translation table, scripts could write junk parental
control values into EEPROM, and XIP reloads trusted the buffer counts.
The library scene's getters and the cache load touched the movie and show lists with no lock while a refresh thread could be swapping them, so browsing during a rescan could read freed strings. They take the library lock now, and a cache that fails to load halfway leaves the old list alone.
Hitting Refresh Library in Playlist Maker kicks a scan that swaps the movie and show lists, while the window kept reading titles and paths out of them every frame. The listing holds the library lock now.
The Xbox file functions now reach the OS through host.h, with one
version for mac and Linux and one for Windows. Folder listing works the
same everywhere, so Windows gets the games.ini-only folders it was
missing. Needs a Windows test.
Library keys, episode order, the games list keeping xCloud and ES-DE rows,
and a handful of threading fixes in Plex and the media library.
Http_GetToFile wrote straight to the final path, so the art loader could pick up half a jpg and a failed fetch wiped whatever was there. It now writes beside the target and renames on success. Needs a Windows test.
Plex, Jellyfin and xCloud ids came straight off the server into cache paths, so an id with a slash or .. in it could write outside the art folder. Anything that isn't a plain filename is skipped now, including xCloud rows whose id becomes an icon name.
A lookup that got no real answer (network down, bad key) came back as not found, and the library saved that as a permanent miss. Now only real search responses get cached, and a failed one stays unlooked up so the next scan tries again. Old cached error pages are ignored too.
Wrote GenerateRadialAlphaMask fresh from the disassembly at 0x56110, with
the per-pixel math, constants and the four callers noted above it. Output
matches what we had pixel for pixel on all four masks.
Wrote both nodes fresh from 0x400f0..0x40320 and 0x418a0..0x41bc0 and
added them to the Settings doc. Reads, writes, file switches, the setting
objects and four screen saver timelines with resets and enable flips all
match the old code step for step.
Wrote both fresh from 0x4f990..0x4fb70 and 0x3ba60..0x3bb70 and wrote them
up in a new Screen doc. Also fixed two wrong Settings rows in the decomp
index. Both builds compile clean and the desktop runs as before.
Read the 75 zones straight out of the 5960 table at 0x201f0 and wrote them
one per row. Every name, bias and change date matches the old table.
Wrote MaxMaterial and every material type's Setup again from 0x56110..0x5a130 and wrote up what the binary does in a new Materials doc. The skin editor color access and the retail table are untouched. Every Setup leaves the same state as the old Xbox file on 121,920 checked draws across all 127 materials, no skin and all 11 skins, and the masks, editor colors and skin reloads match too.
Dropped the desktop copy of the materials and built render/materials.cpp instead. The two NV2A antialias states are Xbox only and the skin editor's frame stamp is desktop only. Every Setup leaves the same state as the old desktop file on all 121,920 checked draws, no skin and all 11 skins.
Wrote CNameSpace, CLerper and the array SetSize calls fresh from the 5960
XBE and added their addresses to the Util doc. Lookups, a lerper timeline
with removals and array growth match the old code exactly. Dropped the
unused namespace Dump.
Wrote Reset, Fetch, FetchFile and MakeUnicode fresh from 0x2e560..0x2e840
and added them to the FileUtil doc. Fetch results match the old code on
every URL kind against the real Data folder, low memory and failed reads
included. Dropped Update and the stream reader, which nothing used.
NewFailed now follows the 5960 out of memory sequence, texture cache then
mesh cache then give up, with the same retries as before on every mix of
cache results. Dropped the assert handler, hex dump and class dump that
nothing called.
Added what the 5960 Setups do that ours skipped: the base Setup and the icons reset stage 0's coordinate index and transform flags, FreeSpace resets stage 1's index, MotdIcon draws without the soft edge, the Live keys take the pulse clock, and the extra stock logo names load from the XIP. Checked that those are the only state changes across all 127 materials. orangeEggGlow is still a plain color; its camera space projection needs texture coordinate generation the desktop renderer doesn't have.
Wrote the binding, the properties, the secret key and the event rules of
Advance fresh from 0x40620..0x41800 and wrote them up in a new Input doc.
The SDL pad and keyboard reading stays as it was. Every event matches the
old code over 4,000 scripted frames of pad, keyboard, rebinding and secret
key input.
CanDriveBeCleanup is written fresh from 0x3b460 and gives the same answer as
before for every character. The rest of the desktop Config is desktop stubs.
Dropped a password encoder nothing called. 5960 allows F to M, T and U where
Theseus allows H to O, T and U; left as is.
Wrote both fresh from 0x32950..0x32ba0 and 0x606d0..0x60930. The Math
constants match the binary's bits, and DotField spacing, drift and every
drawn frame match the old code bit for bit over 5,000 frames.
Wrote the array, its element references and the five script methods
fresh from 0x30440..0x30e70. Contents, lengths, errors and reference counts
match the old code over every constructor form, out of range writes,
slices, sorts and joins. Like 5960, arrays made by concat, reverse, slice
and sort aren't typed as arrays, so concat refuses them; left as is.
Nothing called the software blits, DrawImage, GetImageSize, FetchImage, the RAW decoder, LoadTextureFromXPR, CreateMesh or CMesh::Create from memory, on either build. Removed them and their declarations. The image cache stays since the skin preload fills it.
Locale, date, strings, file paths, settings files and nodes, screen,
timezone, util, activefile, joystick, config, math, dotfield and arrays
written from the 5960 binary; desktop's separate locale.cpp is gone. Also
fixes desktop language files losing every non-ASCII character.
Took main's Math, Array, DotField and Settings nodes over the copies rebuilt here, and its memory hooks. The ScreenSaver index rows now point at the Settings doc.
Rebuilt the texture nodes, the texture cache and its background loader, the XPR parser, CMesh, the Mesh node and MakeSphere from the 5960 XBE. The skin folder probing, loose file fallbacks, skin preload and the flushes are kept as they were. ParseTexture gives the same result as before on all 294 shipped .xbx files plus broken variants of each, 19404 cases.
When D3DX couldn't make the sphere, MakeSphere released the buffers, deleted the mesh, which released them again, and then went on to repack the vertices through a NULL mesh. It deletes the mesh and returns NULL now, which the Sphere node already handles. Desktop had its own guard for this.
Mesh nodes delete their mesh through CMeshCore, which had no virtual destructor, so CMesh never released its vertex and index buffers. Every skin switch leaked the buffers of every loose mesh. Gave CMeshCore a virtual destructor; a flushed mesh's vertex buffer now drops to zero references on desktop instead of staying at one. 5960 has the same leak.
Dropped the desktop copies of the texture, image and mesh code and built render/asset_loader.cpp instead, with the desktop decoders, preload scan and inspector names behind seams. Every mesh, XIP mesh, texture and cache step in my test gives the same result as before under no skin and all eleven skins, 10861 checks. A mesh that fails to load now leaves an empty mesh on the node like on Xbox, which draws nothing either way.
5960 has a function that throws a url out of the texture cache, and its MOTD clear calls it for the .xbx it just deleted. Theseus deleted the files but kept the textures cached. Added UnloadTexture and call it for both MOTD files. Tested on desktop: a cached texture leaves the cache under any case of its name, an unknown name reports false. Needs an Xbox Live Today test.
Materials, camera, animation nodes, starfield and the asset loader written
from the 5960 binary; desktop's separate materials, image, texture and mesh
files are gone. Also stops every skin switch from leaking loose mesh
buffers.
Assigning to a negative index wrote in front of the array, and slice with a
negative start or an end past the last element read outside it. 5960 does
both; scripts get the out of bounds error or a clamped slice now.
rnd() divided rand() by 32767, the Xbox RAND_MAX. On mac and Linux RAND_MAX
is about two billion, so the starfield, dotfield and texture effects got
values far past what they asked for. It divides by RAND_MAX now, which is
the same thing on Xbox.
When a new [Playlist:] section started, the item still being read from the
previous playlist was carried into the new one, through a pointer the new
section could already have moved.
config.xap read AdvancedMode in initialize(), which runs before an Inline
can see default.xap, so GetSavedBooleanValue came up as an unknown function.
It reads the setting when the list is built now.
Opening the panel ran OnActivate before its own scene had loaded, so building the list hit an empty children array (Array reference out of bounds, line 593). The first time in, the panel's onLoad builds the list instead.
@MrMilenko
MrMilenko merged commit 7eb0888 into main Nov 12, 2026
5 checks passed
@MrMilenko
MrMilenko deleted the release-0.4.0 branch November 12, 2026 22:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment