Skip to content

Xbox Live without a console backup: generated console, Insignia registration, updater from the player's dashboard - #108

Open
phobos665 wants to merge 26 commits into
mainfrom
feat/xbl-registration-eeprom
Open

phobos665 wants to merge 26 commits into
mainfrom
feat/xbl-registration-eeprom

Conversation

@phobos665

Copy link
Copy Markdown
Owner

Xbox Live (Insignia) for players who have no backup of a console: no BIOS, EEPROM or HDD key. The runtime makes a console of its own, as hakuX does, and the player brings only their own copy of the 5960 dashboard and a subscription code from Insignia.

What it adds

  • A generated console (console_identity = generate, the default with syslink = nat). It's made once in the per-user console/ folder and never replaced: a random serial, a MAC starting 00:50:F2, a random online key, and hakuX's shared HDD key. A backup still wins when there is one. src/kernel/xbox_console_id.c.

  • Console registration with Insignia from the host (src/net/insignia_register.c). It sends the same HTTPS request Insignia's setup assistant makes, through WinHTTP on Windows and libcurl loaded at run time elsewhere.

  • The launcher's Xbox Live mode, on the System Link tab:

    • Register this console;
    • Dashboard: copies the player's own 5960 dashboard off their xemu disk image (qcow2 or raw) or from a C: folder, and checks it by SHA-1. A short guide is on screen;
    • Updater: in a release, builds the dashboard's online updater from that dashboard.

    It also fixes a hand-set syslink = nat being rewritten to off on Play.

  • src/hdd: reads xemu's qcow2 images and the FATX volumes on them, written from the formats' descriptions.

  • Self-building releases:

    • xboxrecomp-build updater --dashboard <folder> builds the online updater from the player's dashboard;
    • xboxrecomp-build title-update builds a title's own update once the updater has installed it;
    • the launcher starts the update build by itself. Release recipes opt in with "updater": true and "title_update".
  • titles/xodash-online, the 5960 online dashboard recompiled for study only, never shipped. It's how the account requests were mapped (docs/technical/xbox-live-accounts.md). Making an account is still a manual step using it.

  • tools/xbl decode: lays out the Kerberos messages in a RECOMP_NET_PCAP capture, with the encrypted parts as lengths only and the serial masked.

  • Two shared runtime fixes:

    • drive-letter links are found whatever their case;
    • an immediate-mode quad collects the vertices the title's own code wrote when only Begin/End are replaced.
  • Docs:

    • docs/dashboard-files.md, how a player gets their own dashboard. It's shipped in releases beside the README;
    • the Xbox Live guide's no-backup section;
    • docs/technical/xbox-live-accounts.md;
    • the self-build release doc.

Tested

  • Unit tests, all passing on macOS: console_id, insignia_register (a real POST through libcurl to a stand-in server on 127.0.0.1), xbox_hdd (also checked against qemu-img's own qcow2 output: v2, v3, 512 B to 2 MB clusters), and the builder's tests (8).
  • Against Insignia, on macOS:
    • a generated console registered (HTTP 200);
    • an account was made on it;
    • Rainbow Six 3 with update 0x501 signed in with that account and stayed connected through the security gateway.
  • A real macOS release of Rainbow Six 3, whose own builder built three things: the game, the updater from the 5960 dashboard, and update 0x501 from the installed copy. The game handed over to the update's build, which ran offline at 60 fps.

Not tested yet

  • Windows and Linux builds of the new code. WinHTTP has never been compiled.
  • Needs the Windows regression (TS2 and BLiNX) before main: the two shared fixes above change Windows behaviour.
  • The launcher's Updater row and its automatic update build clicked through in a release, and a title update downloaded through a release's updater.

With console_identity = generate (RECOMP_CONSOLE_IDENTITY), the default
under syslink = nat, a console is made once in the per-user console/
folder when no backup is found: a random 12-digit serial, 00:50:F2 plus
three random bytes, a random online key, and hakuX's shared HDD key.
It is written as the eeprom.bin + hd_key.txt the loader already reads,
plus a `generated` marker, so every consumer is unchanged.

Never replaced or written over a backup: to Insignia a new identity is a
new console. A generated console ignores keys.ini's hd_key, and its
video standard follows the title.

xbox-live.md: the generated console, the setup assistant's registration
request, and the config-area sector map (8 network settings, 11 user
table, 12-19 users).
insignia_register.c makes the one request Insignia's setup assistant
makes: POST https://insignia.live/api/v1/xbox with the console's
serial, HDD key and online key as JSON (keys in upper-case hex, as the
assistant formats them). It reads Insignia's answers the way the
assistant does: 200 registered, 422 with Laravel's validation errors
("The serial number has already been taken."), 429 too often.

WinHTTP on Windows; elsewhere libcurl, loaded at run time, so nothing
new is needed to build. A zero HDD key or a bad serial is never sent.
The User-Agent is DirectXbox-Recomp/<version> (RECOMP_INSIGNIA_UA to
change it); RECOMP_INSIGNIA_URL points it at a test server.

tests/insignia_register: the body, every answer, and one request
through the real transport to a stand-in on 127.0.0.1.
The System Link tab gets an "Xbox Live (Insignia)" choice (syslink =
nat). Before it, a hand-set nat loaded as Off and was written back as
off on Play.

In that mode a "Register this console" row (a new action row: A or a
click does it) sends the console to Insignia on a thread of its own and
shows Insignia's answer. The launcher builds the game's own
xbox_console_id.c in, so it finds the console the game will be: a
backup beside the game, else the per-user one. Looking never makes a
console (xbox_console_id_generate(0)); pressing Register makes one when
there is none. A generated console registered from here gets a
`registered` note and is never sent again; a backup's folder is never
written to.

RECOMP_LAUNCHER_TAB / RECOMP_LAUNCHER_ROW open the launcher on a tab and
row, so RECOMP_LAUNCHER_SHOT can draw any of them.
Service 5 (xuacs) requests, the table that drives them, the builder of
each, and how a body is written (16-bit-length UTF-8 text fields). The
plan for making an account from the launcher without a dashboard.
…eturns

reservename, verifyvouchergetoffer, createowneracct, accepttou and
setuserpin, from the 5960 online dashboard's Xbox Live library. The
create reply is the account (XUID, key, domain, realm) the library turns
into the config area's 108-byte user record; the PIN is never sent.
The object manager ignores case. The 5960 online dashboard links
\??\b: to \Device\CdRom0 and opens b:\media\...; the path layer
looks the link up as \??\B:, and the exact compare never found it, so
every lower-case link was recorded and ignored (the dashboard's first
asset failed and it rebooted with a fatal error).
Recompiled to watch how it makes an Xbox Live account
(docs/technical/xbox-live-accounts.md); never shipped. Its files are the
player's own C: copy (games/xodash-online). RECOMP_XUACS_LOG=<file>
writes every Xbox Live request body and each answer, before the gateway
encrypts them, to a private file. Boots and draws at 60 fps with 22 seeds;
it then waits on a loading spinner.
…self

With Begin and End replaced and SetVertexData* not (the 5960 online
dashboard: its symbols are not identified), the vertices between them
are in the push buffer, written by the title's own code, and End's
push_skip walked past them without drawing: every such quad had no
vertices. End now walks them in while the group is open. A title whose
SetVertexData* are replaced writes nothing there, so nothing changes
for it.
The dashboard's sign-up screens close at once when it starts straight
into NEW ACCOUNT, so RECOMP_XUACS_DRIVE=<file> makes the requests from
inside XOnlineTaskContinue instead, through its own Xbox Live library on
the console it signed in: reserve (reservename) and offer
(verifyvouchergetoffer), each pumped and closed so the close hooks log
the answer. There is no create step. Checked offline: the reservename
body is machine ID, country, count, length-prefixed name.
The driver runs from the DirectSound worker's Sleep(100), since the
dashboard stops calling XOnlineTaskContinue once its sign-in task
closes, and can download a dashboard file (service 11), as the
dashboard fetches its country list. Against Insignia: the list
(GB = 35), reservename 200 OK, verifyvouchergetoffer 200 OK with
X-Err 1 and no body (docs/technical/xbox-live-accounts.md).
reservename's 2-byte body is 0 alternatives (the name is free); the
code check's X-Err 1 is stored apart from the request's result, which
succeeds, so a dashboard goes on as a console would. E-mail at owner
+0x624 and the card at +0x5EA/+0x5EC, confirmed in the validators.
Builds the sign-up state (gamertag, no PIN, an adult birth date) and the
owner record with the made-up details Insignia's guide asks for (a test
Visa, a test name and address) and the player's e-mail, and runs only
with confirm_create = yes, since it spends the subscription code. The
library turns the answer into the account on the disk itself. Checked
offline: the body is the layout the docs give.
The code check answers X-Err 1 whatever the code; create with a code
and e-mail that do not match answers 500, X-Err 0x80154007.
The owner record's last text slot (+0x8A6, 26 characters) is the code,
without its dashes: the dashboard's code screen keeps it there and the
code check reads it from there. Left empty, Insignia answered 500 with
X-Err 0x80154007.
reservename, verifyvouchergetoffer and createowneracct from
titles/xodash-online on a generated, registered console: Insignia
answered with the account, written to sector 12 by the library.
Generated console, registered and given an account from the host side;
RS3's update 0x501 signed in with it through Insignia's gateway. The
terms of use were never needed.
On the Xbox Live tab's Register row, A and Enter register the console,
so the footer no longer says they play.
The start of our own Xbox Live client: a pcap reader, DER both ways, and
'python -m tools.xbl decode', which prints what each Kerberos message
in a RECOMP_NET_PCAP capture holds (padata types, principals, realms,
etypes) with lengths for anything encrypted and the serial masked. On a
real sign-in: AS for the machine account, then a TGS per gateway.
The player's one requirement for Insignia without a console backup is
their own 5960 dashboard (a console's C: drive), with the subscription
code Insignia e-mails. docs/dashboard-files.md says how to get it, from
a modded Xbox over FTP or from an xemu disk image with fatx; the Xbox
Live guide's no-backup section gives the steps, and the launcher's Xbox
Live panel points to both.
src/hdd/xbox_hdd.c reads an Xbox disk image, raw or qcow2 (versions 2
and 3, as xemu writes them; compressed, encrypted and backed images are
refused with what to do), and the FATX volumes on it: extract a drive's
whole tree to a folder, or read one file. Written from the formats'
descriptions, with no code of QEMU's or XboxDev's, so a player can give
the launcher the xemu image their own dashboard is on.

tests/xbox_hdd: a FATX volume made by the format's rules, raw and as
qcow2. Checked as well against qemu-img's own qcow2 output (v2, v3,
512 B, 64 KB and 2 MB clusters): the same bytes as the raw image.
…mage

In Xbox Live mode a Dashboard row asks for the xemu hard disk image the
player's own 5960 dashboard is on (raw or qcow2), copies its C: drive on
a thread into the settings folder's dashboard/ (through dashboard.new,
so a failed copy leaves the old one), and checks xodash\xonlinedash.xbe
is the 5960 build by size and SHA-1. Only the two folders it makes are
ever deleted. src/hdd gains SHA-1 (FIPS 180 known answers in the test).

Not yet run against a real xemu image: the readers are checked on
qemu-img's own qcow2 output, the launcher row only drawn.
[XUACS] RESULT ok, or RESULT failed <step> 0x<hresult>, and exit 0 or 2,
for every way a run stops, including no answer in 60 s. Checked
offline: no network, it fails at reserve with exit 2.
xboxrecomp-build updater --dashboard <folder> builds the 5960 dashboard's
online updater (titles/xodash-update, carried as recipe-updater/ when a
recipe says "updater": true) from the dashboard the launcher copied off
the player's xemu image: checks update.xbe's SHA-1, mirrors the
dashboard as updater/game with default.xbe its update.xbe, lifts and
builds there, and installs updater/xodash_update_recomp beside the game,
where the runtime already looks for it. lift and build take a Target
(recipe, exe, XBE, lift hook) instead of the release's game alone.

The launcher's Xbox Live mode offers it as an Updater row once a 5960
dashboard is ready, and shows the build's progress.

Checked on macOS with a stand-in release: the updater lifted with its
seeds and overrides, built, and ran offline at 60 fps. A new builder
test covers the updater's game folder.
…ll as an image

With a Dashboard row selected, the Xbox Live panel says where a player's
dashboard comes from (their own Xbox's C: drive, 5960) and the two ways
in: a folder copied over FTP, or the xemu disk image they installed it
on. A new row takes the folder: copied into dashboard/ with the same
5960 check and swap-in as an image. Releases carry docs/dashboard-files.md
beside README.txt (and beside the app on a Mac), and the README points
to it, so the on-screen guide's "beside this launcher" is true.
xboxrecomp-build title-update builds a title update the dashboard's
updater has installed (TDATA/<TITLEID>/$u/default.xbe, SHA-1 checked)
with the release's title_update recipe: the title's own lift and CMake
switches, in update/, installed beside the game where it hands over to
it. make_release.py passes a recipe's title_update through; the launcher
starts the build itself, on the build screen, when it opens on an update
installed and not built, and a failure leaves the game playable.

Checked with a real macOS release of Rainbow Six 3: game, updater and
update 0x501 all built by the release's builder; the game handed over to
the update's build, which ran offline at 60 fps.
phobos665 added a commit that referenced this pull request Oct 30, 2026
…am, host visibility tests, seeds) (#109)

Toolkit fixes from bringing up **Constantine** (5343000F, XDK 5849).
With these it boots, plays its Bink intro movies, reaches the main menu
and plays its first level at a native 60 fps, with Constantine and his
apartment drawn. The title project itself stays local (`titles/` is
ignored).

## Changes

- **An immediate-mode draw at `End` runs the program the XDK started**
(`c3bd8eba`). Swap scales the 1280x480 supersampled screen down with one
triangle: the XDK starts a two-instruction program (`oPos = v0`, `oT0 =
v9`) through the push buffer and calls `Begin` / `End`. `End` drew it
under the title's 2D program, which reads its texcoord from `v2`, so
every pixel sampled one texel and each frame after the movies was a
single flat colour. `End` now uses `pushed_program_begin` as the walk's
own draws do.
- **Visibility tests are answered from host occlusion queries**
(`dd28b57d`). They were answered "0 pixels". Constantine culls its
scenery and its hero by them (53 a frame), so neither was drawn.
- The walk now hands `SET_ZPASS_PIXEL_COUNT_ENABLE` (0x17CC) and
`GET_REPORT` (0x17D0) to the HLE.
- The RHI counts samples in between: new `rhi_occlusion_*`, one Vulkan
query per draw, one D3D11 query per test.
- `GetVisibilityTestResult` answers with the matching test's count,
scaled to guest samples.
- All closed tests are read back in one GPU wait. One at a time it was
about 80 waits a frame.
  - `RECOMP_HLE_D3D8_VISIBILITY=0` switches it off.
- **Frame dumps and F11 see frames shown through the video overlay**
(`39dbec03`). They now catch movies presented by `UpdateOverlay` without
a Swap.
- **A thread that only polls the clock sleeps between polls.**
Constantine's Bink player checks whether the next frame is due (a mutex
taken and released) and reads the TSC in a loop between movie frames:
about two million kernel calls a second, 83% of a core, during every
movie. A thread that reads the TSC 256 times within 1 ms, four windows
running, now sleeps 1 ms with the guest lock dropped.
`RECOMP_TSC_POLL_SLEEP=0` turns it off. Over 50 s of intro movies,
main-thread CPU went from 44.2 s to 6.3 s, with the same movie frames at
the same times. In-level uncapped is unchanged.
- **A switch's index walk stops at code something names** (`f61788a5`,
`tools/disasm`). The disassembler's walk over a switch's index bytes
treated `56 8B` (`push esi; mov esi, ecx`) as two more indices, because
the switch has about 156 cases. It started a function in the middle of
the `mov`, so the lifted method lost `esi = ecx`, its vtable calls went
wild, and Select Difficulty showed no options. The walk now stops at a
plausible function start that's named as code. Compared with the old
disassembler, Constantine differs by exactly that one bogus entry.
**Other titles change only when re-lifted.**
- **Seeds for Constantine** (`259d78e7`): six thread starts, plus a
callback (0x001DF280) that the disassembler had run into from a jump
table before it. It was called over 1000 times unresolved.
- `1e4536a8` is the same immediate-mode vertex fix as in #108,
cherry-picked here.

## Measured (macOS arm64, Vulkan)

- In-level, capped: a steady 300 swaps per 5 s (60 fps).
- Uncapped: about 430 swaps per 5 s, against about 505 with
`RECOMP_HLE_D3D8_VISIBILITY=0`. That run also draws less, because the
culled objects aren't drawn.

## Not done / needs checking

- **The D3D11 half of `rhi_occlusion_*` has not been built or run.** It
needs a Windows build, plus the TS2 and BLiNX regression before merging,
because the `End` change, the visibility tests and the clock back-off
affect every title.
- Still open on Constantine:
  - 7 shadow-map draws read all-zero vertices;
  - 4 of 8 sound buffers are never heard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant