Skip to content

Working Linux Build - #45

Open
BMagnu wants to merge 1 commit into
sal063:mainfrom
BMagnu:Linux
Open

Working Linux Build#45
BMagnu wants to merge 1 commit into
sal063:mainfrom
BMagnu:Linux

Conversation

@BMagnu

@BMagnu BMagnu commented Aug 28, 2026

Copy link
Copy Markdown

This is a fork with most fixes and enhancements from master working on linux.
Closes #4, closes #6, closes #12
I've tested this as far as the end of Mission 1 so far, with everything working as expected so far.
Note: Heavily vibecoded, so some of the code is going to be less pretty than it should be.

First, a list of the features I have not yet ported, with no immediate plan due to the amount of effort needed to make them work on linux:

  1. Texture replacement modding
  2. KBM support
  3. Networking (not sure how much AC6 makes use of that).

Also, I did NOT test this on windows. Someone with a windows machine should definitely give this a run before anyone thinks about merging this.

Now, an AI-gen'd list of changes for the main build system and OS-dependent code:

1. Build the Vulkan backend and the AC6 backend fixes on Linux

ac6_widescreen.cpp and ac6_fullres_effects.cpp were Win32-only — __try/__except,
VirtualQuery/VirtualProtect, _byteswap_*, CreateThread. The platform-independent logic
(the camera signature predicate, the aspect poke, the static-default patch) is now factored out and
shared, with only the memory-probing primitives behind the platform split.

CMakeLists.txt moves the texture-swap cvar definitions into src/ac6_texture_swap_cvars.cpp so
they exist on every platform while ac6_texture_overrides.cpp — which needs the D3D12/DXGI format
enums — stays Windows-only, and declares the rexsystemrexgraphics link back-edge that a
Vulkan-only build needs (on Windows the symbol resolves by accident through an earlier reference).

2. Guest-memory probing that cannot fault

Guest memory is an shm mapping, so a page can be readable and still unbacked: a plain dereference
raises SIGBUS, for which no handler is installed. Every probing read now goes through
process_vm_readv, which performs the access in the kernel and reports EFAULT instead of
signalling. Writes stay direct (after a probing read validates the mapping) so a write to a
GPU-write-watched page still faults into the SDK's recovery handler and is tracked like any other
guest write.

The widescreen memory sweep has no VirtualQuery to walk regions with, and parsing
/proc/self/maps per sweep is far too slow, so it strides the guest range in 1 MiB chunks read
through the same call — an unmapped range simply fails — and scans the copy, taking writability
from the SDK's own page tracking.

3. QueryProtect protection shadow

QueryProtect answered by parsing /proc/self/maps: a file open plus a ~1000-line scan. The MMIO
handler calls it on every access violation, while holding the global critical region, and guest
write-watches fault constantly — this measured at 39% of total process CPU with the guest making
no forward progress at all. Windows has no equivalent problem, VirtualQuery being a cheap syscall.

AllocFixed, Protect and DeallocFixed are the only writers of this process's own protections,
so they now record them in a coalesced, page-aligned range map answered in O(1). A lookup that
misses still falls back to the maps file, so addresses this module never handed out (host
allocations, the stack) answer exactly as before.

4. NT event semantics for POSIX waits

The Win32 dispatcher hands an auto-reset release to one specific waiter, in FIFO order. Publishing a
flag that every waiter races for is not equivalent in two ways that both strand the guest: two
releases delivered before any waiter runs collapse into one, and no waiter is guaranteed to ever
win. Signallers now grant a token to the longest-waiting thread instead
(posix_event_fifo_handoff, on by default — off restores the old flag behaviour so a suspected
regression can be A/B'd without a rebuild).

Alongside it: a log_long_waits_ms watchdog that names the stuck thread, the object and the wait
kind from a fixed-address table readable out of a core or a live process, and a sync_tests binary
covering the primitives (kept separate from unit_tests so it needs only rexcore). The watchdog
logs at error level, because ac6_performance_mode pins the log level to error and that is exactly
the configuration a hang gets reproduced in.

5. POSIX file handles

O_RDONLY/O_WRONLY/O_RDWR are an enumeration in the low two bits (0/1/2), not bit flags, so
they cannot be OR-ed together: a read+write open OR-ed to O_WRONLY and every read on that
descriptor then failed with EBADF. The title opens its save read+write, so the reads silently
returned nothing and it wrote back whatever its buffer already held. Windows has no equivalent
problem — GENERIC_READ|GENERIC_WRITE really are bit flags.

Also: pread/pwrite returning -1 was assigned straight into a size_t out-parameter, reporting
SIZE_MAX bytes transferred to any caller that checked the count rather than the bool.

6. Crash and fault diagnostics

An unclaimed SIGSEGV returned from the handler, which re-executes the faulting instruction, which
faults again — the thread spins inside the signal handler forever, burning a core, with no
diagnostic anywhere. A guest null dereference therefore appeared as an unexplained "deadlock"
instead of a crash. It is now reported (translated back to a guest address), handed to a
guest-aware reporter, and then allowed to die under SIG_DFL, producing a core that points at the
real faulting instruction.

diag_crash_handler.cpp gains its POSIX half. Rather than installing a competing SIGSEGV
handler — ExceptionHandler owns that signal — it registers as that reporter and prints what a
host backtrace cannot give: the guest call chain walked from the saved stack backchain, all 32
GPRs, and the memory behind every register that looks like a live guest pointer. It runs on a
thread that is about to die, so it takes no locks and probes every read first.

7. Guest sockets no longer block forever

XNet is not implemented, so nothing can ever deliver to a guest socket and a blocking recv on one
waits forever — the title does not get past its network init. guest_socket_recv_timeout_us
(2 ms default) bounds the wait. This is a stopgap: it makes a socket the guest expects to block
return EAGAIN instead, and should be removed if real Xbox 360 socket semantics land.

8. We were injecting RenderDoc into ourselves

RenderDocAPI::CreateIfConnected() is meant to hand back the API only when RenderDoc has already
attached. On POSIX it probed with a plain dlopen("librenderdoc.so", RTLD_LAZY), which loads
the library when it is not already mapped. librenderdoc.so lives in a system library directory on
any machine with RenderDoc installed, so every run pulled RenderDoc in, initialised it, and got its
capture overlay drawn over the game — and, through IsGpuDebugMarkersEnabled()'s "auto-enable when
RenderDoc is detected" path, silently turned gpu_debug_markers on as well.

Windows has the same shape but not the same outcome: renderdoc.dll is not on the default DLL
search path unless RenderDoc genuinely injected it. The probe now uses a new
DynamicLibrary::LoadIfAlreadyLoaded, which is RTLD_NOLOAD on POSIX and unchanged
LoadLibraryW on Windows, so it observes RenderDoc rather than causing it. Launching under
RenderDoc still works exactly as before.


Vulkan backend

9. AC6's backend hooks were D3D12-only

A whole family of AC6 fixes was wired into the D3D12 command processor only. On Vulkan they
reported themselves enabled in the log and did nothing. Ported:

  • World-compositor draw notification — the per-frame "3D world is rendering" signal. Without
    it AreTimingHooksActive() fails closed forever and the entire FPS-unlock / physics-dt family is
    silently dead.
  • Full-res effects — the compositor mask crop, and the 2:1 silhouette downscaler fix (the draw
    that produced the 2×2 ghost planes).
  • Ultrawide — screen-space UI ortho patching in the VS float constants, sub-viewport
    (radar window, PiP inset) viewport and scissor shrink about the render-target centre, target
    marker quad narrowing, and the mode-classified swap-source notification that decides fill vs.
    letterbox presentation.
  • HD terrain — the synthetic vertex-fetch-95 ring table, its residency redirect and its
    fetch-constant patch.
  • Condensation trails — the physical-memory invalidation callback that drops cached vertex
    buffer residency when the guest rewrites its trail history ring in place, and the forced RT0
    colour mask for the trail point-list pass (whose register state writes no colour components, so
    the draw was dropped entirely).
  • Sun flare — the second, malformed billboard cull, previously only in the DXBC translator.
  • De-swizzle neutralisation — the identity host-texel UV for allowlisted post-process passes.

10. Texture result exponent bias read from the wrong fetch-constant word

The SPIR-V translator took the result exponent bias from bits 13:18 of fetch constant word 4.
Those bits fall inside lod_bias (dword_4 +12, 10 bits); the actual exp_adjust field is at
dword_3 +13. A non-zero LOD bias was therefore applied as an exponent bias, scaling every fetched
texel by a power of two — for AC6 by 2⁻⁸, crushing the composite to black. The DXBC translator
reads word 3, which is why this only ever affected Vulkan.

11. execute_unclipped_draw_vs_on_cpu restored to its upstream default

With it off, unclipped draws get no vertex extent estimate, so height_used falls back to the full
render-target height. A single depth-only unclipped draw then claims all 2048 EDRAM tiles and takes
ownership of every range, and anything resolving from those tiles afterwards inherits depth data
instead of colour.

12. Diagnostics on the silent failure paths

Several Vulkan paths returned false or continued without a word, leaving only a black result to
work back from. Once-only error logs added for: a failed texture upload, a texture with no load
shader or a null load pipeline, a transfer with no buildable pipeline, and a render target that
would transfer to itself. VulkanPipelineCache also honours dump_shaders now — only the D3D12
pipeline cache dumped its translated binary, so the setting produced no SPIR-V at all.


Audio

13. SDL output sized to the real device period

The driver hardcoded a queue target of 3 render-driver frames while SDL opens a 1024-frame period
at 48 kHz — four 256-sample guest frames. The host therefore asked for more audio than the runtime
was ever allowed to queue, and the shortfall was filled with silence on every callback.

The period is now queried from the device and the queue target derived from it, with two frames of
headroom for worker wake jitter (the POSIX multi-handle wait is a 1 ms poll, not a real blocking
wait). Supporting changes:

  • The frame pool is pre-allocated as one backing store, so the realtime callback never contends
    with a thread inside the allocator; on exhaustion the stalest queued frame is recycled rather
    than blocking, and the drop is counted in telemetry.
  • Silence injection writes only what was actually asked for and reports only what it actually
    wrote — the guest render-driver tic is derived entirely from ReportSamplesConsumedForClient,
    so over-reporting ran the guest's audio clock ahead of real playback.
  • audio_max_queue_depth raised to 16, and a clamp below what the driver needs now warns instead
    of silently guaranteeing an underrun.
  • A safety valve releases the startup callback pacing after a bounded number of throttled passes,
    so a starved host cannot wedge startup indefinitely.
  • AudioTraceBuffer::Record early-outs when tracing is off. It is reached from the host audio
    callback on every consumed frame, and took a mutex and churned a deque there for data nobody
    reads.

Input

14. Sticks the kernel binds to hid-generic were silently invisible

This one is technically an enhancement because I'm not sure how windows handles joystick to gamepad for rexglue.

A Thrustmaster T.Flight Hotas One (USB 044f:b68d, the AC7-branded flight stick) produced no input at all and, worse, produced no log line either — it was indistinguishable from a device that was never plugged in. Five things had
to line up for that:

The kernel's xpad table has no entry for b68d, so the stick binds to hid-generic. That driver
describes it with joystick-class button codes (BTN_TRIGGER…) rather than the gamepad-class
codes (BTN_GAMEPAD/BTN_SOUTH…) an xpad device would report. SDL3's Linux backend will
synthesise a gamepad mapping for an unknown device, but LINUX_JoystickGetGamepadMapping gives up
immediately on !has_key[BTN_GAMEPAD] — "not a gamepad according to the specs". Neither SDL's
built-in mapping table nor the upstream community SDL_GameControllerDB has an entry for this GUID.
And SDLInputDriver reaches devices exclusively through the SDL gamepad API: it inited only
SDL_INIT_GAMEPAD and listened only for SDL_EVENT_GAMEPAD_ADDED, which an unmapped joystick never
raises. There is no SDL_Joystick fallback anywhere in the runtime.

Three changes, only the first of which is device-specific:

  • A gamecontrollerdb.txt now ships at the repo root and is copied next to the executable on every
    build, carrying a hand-authored entry for this stick. Its throttle is split across both triggers,
    its yaw twist across the shoulder buttons, and its Trim control is an axis rather than a pair of
    buttons, so it drives dpup/dpdown as half-axis bindings. The coolie hat is the right stick,
    which deliberately leaves dpleft/dpright unbound — AC6 does not need them, at the cost of
    UpdateXCapabilities reporting X_INPUT_CAPS_NO_NAVIGATION.
  • hid_mappings_file resolves against the executable directory first, falling back to the CWD.
    Previously it was a bare relative name resolved against the CWD alone, and since no such file
    shipped, that path only ever logged file '...' does not exist. Mappings also load before
    SDL_INIT_GAMEPAD now, so the initial device-added burst is already mapping-aware.
  • SDL_INIT_JOYSTICK is requested alongside SDL_INIT_GAMEPAD purely so unmapped sticks still
    raise SDL_EVENT_JOYSTICK_ADDED. A new handler warns with the device name, VID/PID, and the GUID
    a database line has to be keyed on. The next unrecognised device is a log line rather than a
    hardware investigation.

Incidentally fixed in the same file: OnControllerDeviceAdded/Removed read event.cdevice.which,
the camera device union member, where they meant event.gdevice.which. The two structs are
layout-identical so it worked, but only by accident.


Some more fixes were also incorporated and inspired from a different seemingly WIP fork of this repo targeting linux
A second, independent Linux port exists at
The four fixes in this section are that fork's work, ported here — the analysis and
the original implementations are theirs.

15. The >1x mosaic, and arming the scaling fixes for resolution_scale

("Fix the >1x mosaic on Vulkan, and arm the scaling fixes for resolution_scale").

param_gen_integer_guest_position and param_gen_host_subpixel_restore existed as cvars and were
read by the DXBC translator, but nothing on the SPIR-V side read them — so the scaling fixes
were inert on Vulkan and AC6's deferred EDRAM restore / de-swizzle passes scrambled into a mosaic
at any draw scale above 1x. Two halves, both mirroring the DXBC path:

  • StartFragmentShaderInMain now floors the reverted PsParamGen position to the integer guest-pixel
    index. Reverting the resolution scale leaves a sub-guest-pixel fraction that is only correct for
    shaders feeding PsParamGen straight to a tfetch; shaders doing integer pixel-address maths on it
    see a multiplied period in their frac()-based bit extraction and scramble the sample coordinate.
  • ProcessTextureFetchInstruction re-adds the host sub-pixel after the coordinate is normalized, so
    those passes regain full host resolution instead of sampling at guest resolution. It runs before
    the de-swizzle identity override, which overwrites the coordinate outright — the same precedence
    the DXBC translator uses.

Separately, ApplyAc6FixDefaults gated the whole family on draw_resolution_scale_x/y, but the
combined resolution_scale cvar — what the settings menu writes — leaves those at 1. It now asks
TextureCache::GetConfigDrawResolutionScale for the effective scale, so the fixes engage for
everyone who scaled that way; the effective scale is also logged on the config support line.

16. A POSIX read at end of file must fail

("Report a POSIX read at end of file as a failure, like the Win32 handle").

Win32 ReadFile reports ERROR_HANDLE_EOF, which HostPathFile turns into
X_STATUS_END_OF_FILE. pread just returns 0, which reached the guest as "success, zero bytes,
position unchanged" — a loader reading a file to its end then never terminates. A short or
zero-length write is likewise a failure now, so the atomic-write path cannot commit a truncated
temp over a good file. (This sits on top of our own fix to the same function, item 5.)

17. getCompTexLD cube face id and negated Z

("Produce the cube face id and negated Z whenever they are used").

Two component-mask bugs in the SPIR-V translator's cube coordinate lowering. Negated Z feeds both
the X-major sc (component 1) and the Y-major tc (component 0), but was only created for
component 1; and the Y-major face id was computed inside the tc guard, so it went unset whenever
the id was wanted without the coordinate. Wrong cube-map sampling on Vulkan for any shader
requesting only some components.

18. Stencil-bit transfers killed no samples

("Kill stencil-bit transfer samples whose source bit is clear").

A stencil-bit transfer from a depth/stencil source binds only the stencil texture — the depth is
not needed and deliberately not bound — so neither packing branch ran and packed was left unset.
The sample kill is skipped entirely when packed is NoResult, so every sample kept its bit and
the destination stencil came out 0xFF everywhere regardless of the source.

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.

d3d12.h not found in linux runtime Fails to compile on Arch Linux Arch Linux build--needs build patches, deadlocked at presentation

1 participant