SpeedBreaker v0.1.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
SpeedBreaker ProjectandClaude Opus 5.5 committed 2026-10-08 13:25:54 +00:00
commit a6ade41b9f
290 files changed
+141850

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
# Game data is NEVER committed. Players supply their own disc.
# (Anchored: runtime/game/ is source. No trailing slash: a symlink too.)
/game
private/
*.iso
*.xex
*.xexp
*.xcp
*.bun
*.big
*.lzc
# The disc's other formats, wherever an extracted copy lands: movies,
# archives (ZDIR.BIN, ZZDATA*.BIN) and the PADA*/PADB* files.
*.wmv
*.xmv
*.bin
*.BIN
PAD[AB][12]
# Generated recompiler output (from the player's own default.xex)
ppc/
/ppc
# Build output and packages
build/
out/
.cache/
*.tar.xz
*.dmg
*.dSYM/
*.ipa
xcuserdata/
# Third-party tool checkouts (scripts/setup_tools.sh)
tools/
/tools
# Local survey binary
build-survey
# Frame dumps and captured shaders (game data)
/game/frames/
/game/shaders/
*.ppm
# Logs and bug reports (they hold a screenshot of the game)
last-run.log
previous-run.log
speedbreaker-bug-report-*.zip
# macOS
.DS_Store
# Python
__pycache__/
*.pyc
+50
View File
@@ -0,0 +1,50 @@
# Changelog
What changed in each SpeedBreaker release, newest first. Each release's notes on GitHub start from
its entry here, and the game shows the entry once, the first time a new version starts (offline:
it's built into the game).
## v0.1.0
The first public release of SpeedBreaker: an unofficial static recompilation of the Xbox 360
version of Need for Speed: Most Wanted (2005), running natively. Bring a backup of your own USA
Xbox 360 disc: the installer checks it and installs the game.
### Where it runs
- Steam Deck, Steam Machine and Linux PCs (x86-64).
- Steam Frame, as a big flat screen in the headset (not VR).
- Mac with Apple silicon, on macOS 26 or later.
- iPhone and iPad: there's no download, you build it yourself with Xcode.
- Windows: coming soon.
### What you get
- Up to 4K: the 3D scene renders at 1x, 2x or 3x the console's 720p, and Auto picks one for your
screen and device.
- Ultrawide screens get a wider view instead of a stretched one, and 4:3 and 3:2 screens a taller
one, with the HUD and the rear-view mirror where they belong.
- Up to 60 fps on a 60 Hz display, a steady 30 at full game speed, or Auto, which drops to 30 only
while the device is hot or can't hold 60.
- The original look: the low sun's glare, correct colours on Apple devices, proper car paint and
mirror reflections, and distant textures that don't shimmer.
- A guided installer: it finds your disc image, checks that it's the right game and verifies all
49 files as it copies them.
- Controllers work as an Xbox 360 pad, with hot-plugging and rumble. The keyboard works too.
- A settings menu that pauses the game: F1, Back + Start (View + Menu on the Steam Deck and Steam
Frame), or a three-finger tap on iPhone and iPad. Every change applies at once.
- On iPhone and iPad, switch away and the game freezes; come back and it carries on.
- Settings > Advanced > Save Bug Report puts the logs, your settings and a screenshot in one zip
file. Nothing is uploaded: you decide what to send.
- From this version on, the first start after an update shows what changed, once. Nothing is
checked online.
### Good to know
- Mac: the app isn't notarized by Apple, so the first time, allow it in System Settings > Privacy
& Security > Open Anyway.
- Steam Deck OLED: set the screen to 60 Hz.
- Steam Frame: Display > Frame Rate 30 fps is the smooth choice in the headset.
SpeedBreaker is an unofficial fan project, not affiliated with Electronic Arts or Valve. No game
files are included.
+50
View File
@@ -0,0 +1,50 @@
# SpeedBreaker. GPL-3.0-or-later (see COPYING).
#
# cmake -S . -B build/main -G Ninja -DCMAKE_BUILD_TYPE=Release \
# -DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \
# -DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++
# ninja -C build/main -j6
#
# Needs ppc/ (XenonRecomp output from your own default.xex) and tools/
# (scripts/setup_tools.sh). -DSB_TESTS=ON adds the unit tests (tests/; run
# them with ctest from the build folder).
cmake_minimum_required(VERSION 3.20)
# macOS (not iOS, which scripts/ios/build.sh sets to 17.0): the oldest macOS
# the game runs on, which the Mac app declares (scripts/macos/make_app.py
# reads it from the binaries and refuses a library that needs a newer one).
# Homebrew's bottles for macOS 26 are built for 26.0, so that's the floor
# while the app bundles them. Before project(): it picks the SDK's settings.
if(CMAKE_HOST_APPLE AND NOT CMAKE_SYSTEM_NAME STREQUAL "iOS" AND NOT DEFINED CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "26.0" CACHE STRING "The oldest macOS the game runs on")
endif()
# The version: the one place it is set. Everything else comes from it: the
# log's [build] line, Settings > Advanced, bug reports' names, the Mac and iOS
# apps' versions, the SteamOS bundles' VERSION file and the release tag
# (v<version>). CHANGELOG.md has an entry for each version (the in-game
# "What's new" note is built from it).
project(SpeedBreaker VERSION 0.1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Binaries name their sources from the repository root (assert messages,
# debug information), never by the folders they were built in: a published
# build carries no one's home folder (a build folder inside the checkout
# reads ./build/...). The real path too, for a checkout reached through a
# symlink; both map to the same name, so their order doesn't matter.
file(REAL_PATH "${CMAKE_SOURCE_DIR}" SB_SOURCE_REAL)
add_compile_options("-ffile-prefix-map=${CMAKE_SOURCE_DIR}=.")
if(NOT SB_SOURCE_REAL STREQUAL CMAKE_SOURCE_DIR)
add_compile_options("-ffile-prefix-map=${SB_SOURCE_REAL}=.")
endif()
add_subdirectory(recomp)
add_subdirectory(runtime)
option(SB_TESTS "Build the unit tests (tests/)" OFF)
if(SB_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2026 Omar Cornut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+502
View File
@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+18
View File
@@ -0,0 +1,18 @@
Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2017 Evan Nemerson <evan@nemerson.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+13
View File
@@ -0,0 +1,13 @@
Copyright (c) 2012-22 SAURAV MOHAPATRA <mohaps@gmail.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+24
View File
@@ -0,0 +1,24 @@
Copyright (c) 2015, Ben Vanik.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the project nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL BEN VANIK BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+21
View File
@@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2025 hedge-dev and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+20
View File
@@ -0,0 +1,20 @@
Copyright (c) 2012 Rob Clark <robdclark@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1013
View File
File diff suppressed because it is too large. Load diff
+504
View File
@@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Pavel Kirienko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+18
View File
@@ -0,0 +1,18 @@
ALTERNATIVE A - MIT License
Copyright (c) 2017 Sean Barrett
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
+16
View File
@@ -0,0 +1,16 @@
MIT License
Copyright (c) Mark Gillard <mark.gillard@outlook.com.au>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+37
View File
@@ -0,0 +1,37 @@
SpeedBreaker
Copyright (C) 2026 project(u) and SpeedBreaker contributors
SpeedBreaker is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or (at your option) any later
version (GPL-3.0-or-later). The full text of version 3 is in COPYING.
SpeedBreaker is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
SpeedBreaker includes, and is built with, software by others under their own
licenses: see THIRD_PARTY_NOTICES.md and the LICENSES folder.
Source code: SpeedBreaker's own is at
https://github.com/SpeedBreakerProject/speedbreaker, and each download names
the commit it was built from (in VERSION, or the Mac app's Info.plist). The
downloads also contain code by others under the GNU LGPL or GPL: FFmpeg (with
SpeedBreaker's patch) and libmspack in every download, and the GNU C Library
and GCC's runtime libraries in the SteamOS x86-64 download. Their complete
source code, at the versions used, is published next to the downloads of each
release on the project's Releases page, as
SpeedBreaker-<version>-third-party-source.tar.xz. If it is ever missing, ask
through the project's issue tracker: for at least three years after a
release, its copy is provided at no charge.
SpeedBreaker is an unofficial, fan-made project. It is not affiliated with,
endorsed by, or sponsored by Electronic Arts Inc., the Need for Speed brand or
Valve Corporation. "Need for Speed" and "Most Wanted" are trademarks of
Electronic Arts Inc. Steam, Steam Deck, Steam Machine, Steam Frame and SteamOS
are trademarks of Valve Corporation. All other trademarks belong to their
respective owners.
No game files are included or distributed. To play, you need your own legally
obtained backup (a disc image) of your own Xbox 360 disc of Need for Speed:
Most Wanted (2005). The in-app installer verifies it and installs it.
+677
View File
@@ -0,0 +1,677 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/speedbreaker-banner-dark.png">
<img alt="SpeedBreaker" src="assets/speedbreaker-banner-light.png" width="560">
</picture>
</p>
<p align="center">
<b>An unofficial static recompilation of the Xbox 360 version of <i>Need for Speed: Most Wanted</i> (2005):<br>
a native port, not an emulator.<br>
Play your own copy on Steam Deck, Steam Machine and Steam Frame (SteamOS), Linux and Mac,<br>
or build it yourself for iPhone and iPad.</b>
</p>
<p align="center">
<a href="https://github.com/SpeedBreakerProject/speedbreaker/releases/latest"><b>Download</b></a>
&nbsp;·&nbsp;
<a href="#quick-start">Quick start</a>
&nbsp;·&nbsp;
<a href="https://projectugarte.com/speedbreaker/">Website</a>
&nbsp;·&nbsp;
<a href="#reporting-a-bug">Report a bug</a>
</p>
<p align="center">
<img src="assets/screenshots/speedbreaker-00-ultrawide-chase-cam-race.jpg" alt="A race on a 21:9 ultrawide screen, seen from the camera behind the player's car: an open expressway into a low sun, with the race HUD and the rear-view mirror" width="100%">
<br>
<sub>What you see in a race, on a 21:9 ultrawide: a wider view, not a stretched one, with the HUD and the mirror where they belong.</sub>
</p>
> [!IMPORTANT]
> **SpeedBreaker is an unofficial fan project by project(u). It is not affiliated with, endorsed
> by or sponsored by Electronic Arts Inc., the Need for Speed brand or Valve Corporation.**
>
> **No game files are included or distributed.** To play, you need your own legally obtained
> backup (a disc image) of your own Xbox 360 disc of *Need for Speed: Most Wanted* (2005). The
> in-app installer verifies it and installs it. Please don't ask for, or share, game files or
> disc images. See the full [legal notice](#legal-notice).
**Contents:** [What it is](#what-is-speedbreaker) · [Highlights](#highlights) ·
[Download](#download) · [Quick start](#quick-start) · [Setup by platform](#setup-by-platform) ·
[Controls](#controls) · [Settings](#settings) · [Tips](#tips-for-smooth-play) ·
[FAQ](#faq-and-troubleshooting) · [What's next](#whats-next) ·
[For developers](#for-developers) · [Credits](#credits) · [License](#license) ·
[Legal notice](#legal-notice)
## What is SpeedBreaker?
SpeedBreaker is an unofficial static recompilation of the Xbox 360 version of *Need for Speed:
Most Wanted* (2005): a native port, not an emulator. It runs as an ordinary app on your device
(see [how it works](#how-it-works)).
It is built from the **Xbox 360 version** of the game, not the PC version: it runs the Xbox 360
release's own code, translated ahead of time, and it installs only from the Xbox 360 disc.
You bring the game: SpeedBreaker installs it from a backup of your own disc and checks every
file. Then you race, with things the Xbox 360 couldn't do: up to 4K, ultrawide and tall screens,
and a settings menu you can open mid-race.
## Highlights
- **Up to 4K.** The 3D scene renders at 1x, 2x or 3x the console's 720p (3x is 3840x2160).
*Auto* picks a scale for your screen and device.
- **Ultrawide done right.** On 21:9 screens the view gets wider instead of stretched, and the
HUD and the rear-view mirror stay in place. A 4:3 iPad or a 3:2 screen shows more above and
below instead. 16:10 screens, like the Steam Deck's, keep thin bars.
- **Up to 60 fps,** locked to your 60 Hz display. You can also pick a steady 30 at full game
speed, or *Auto*, which drops to 30 only while a device is hot or can't hold 60.
- **The sun glare is back.** The low sun glows in open sky and fades behind buildings, as it did
on the console.
- **The original look:** correct colours on Apple devices, proper car paint and mirror
reflections, and distant textures that don't shimmer. Anti-aliasing, sharpening and up to 16x
anisotropic filtering are also available.
- **A guided installer.** It finds your disc image, checks that it is the right game, and
verifies all 49 files as it copies them. An interrupted install never leaves a broken game.
- **Your controller, or the keyboard.** Controllers work as an Xbox 360 pad, with hot-plugging
and rumble support.
- **A settings menu that pauses the game.** Open it with F1, Back + Start, or a three-finger
tap. Every change applies at once.
- **Suspend and resume on iPhone and iPad.** Switch away and the game freezes; come back and it
carries on where it stopped.
- **One-button bug reports.** Logs, settings and a screenshot go into one zip file. Nothing is
uploaded, and you decide what to send.
<table>
<tr>
<td width="50%"><img src="assets/screenshots/speedbreaker-x4-ultrawide-race-sun-sparks.jpg" alt="Racing at 21:9 into a low sun, sparks flying off the wall, HUD and rear-view mirror visible"><br><sub>21:9 in a race: the wider view and the low sun's glare.</sub></td>
<td width="50%"><img src="assets/screenshots/speedbreaker-03-4x3-cinematic-flame-livery-closeup.jpg" alt="A close-up of a black car with flame livery, in a 4:3 frame"><br><sub>4:3, the shape of an iPad screen, with more picture above and below.</sub></td>
</tr>
<tr>
<td width="50%"><img src="assets/screenshots/speedbreaker-01-ultrawide-cinematic-rivals-side-by-side.jpg" alt="The intro cinematic in 21:9: two cars side by side"><br><sub>The intro cinematic at 3440x1440.</sub></td>
<td width="50%"><img src="assets/screenshots/speedbreaker-04-4k-cinematic-pursuit-crash.jpg" alt="Police cars crashing into a barrier in a shower of sparks"><br><sub>A pursuit cinematic at 3840x2160.</sub></td>
</tr>
<tr>
<td width="50%"><img src="assets/screenshots/speedbreaker-10-4k-settings-internal-resolution.png" alt="The SpeedBreaker settings menu over the paused race, on the Graphics page"><br><sub>The settings menu over a paused race.</sub></td>
<td width="50%"><img src="assets/screenshots/speedbreaker-11-installer-disc-image-found.png" alt="The first-run installer, showing a disc image it found, ready to install"><br><sub>First run: the installer has found the player's disc image.</sub></td>
</tr>
</table>
<sub>Screenshots: SpeedBreaker on a Mac Studio (M4 Max). They are unedited frames from the game's
own output, except that the licence plate in the top image is blurred. The game content in them
belongs to its owners.</sub>
## Download
| Platform | How to get it | Tested on |
|---|---|---|
| **Steam Deck** and **Steam Machine** (SteamOS) | [Download][dl-steamos-x86_64]<br>`SpeedBreaker-steamos-x86_64.tar.xz` | Steam Deck OLED (SteamOS 3.9.2). Steam Machine (SteamOS 3.8) on a 3440x1440 ultrawide. The Steam Deck LCD hasn't been tested yet. |
| **Steam Frame** (SteamOS, arm64) | [Download][dl-steamos-arm64]<br>`SpeedBreaker-steamos-arm64.tar.xz` | Steam Frame (SteamOS 0.3.0). It plays as a big flat screen in the headset; it is not a VR mode. |
| **Linux** (x86-64) | [Download][dl-steamos-x86_64] the x86-64 SteamOS build, `SpeedBreaker-steamos-x86_64.tar.xz`, or [build from source](#building-from-source) | SteamOS only so far. Other distributions and NVIDIA or Intel GPUs are untested: reports welcome. |
| **Mac** (Apple silicon) | [Download][dl-macos]<br>`SpeedBreaker-macos.dmg` | MacBook Pro (M1 Pro) and Mac Studio (M4 Max), on macOS 26. There is no Intel Mac build. |
| **iPhone** and **iPad** | [Build it yourself](#iphone-and-ipad) with Xcode | iPhone 15 Pro Max (iOS 27). iPad Pro 12.9-inch (M2, iPadOS 27) with a Backbone Pro. Not on the App Store. |
| **Windows** | Coming soon | |
| **Android** | Coming soon | |
Each link downloads the newest release. Every file and its `.sha256` checksum file are on the
[latest release page][releases]. To check a download, put its `.sha256` file next to it and run
`shasum -a 256 -c SpeedBreaker-macos.dmg.sha256` on a Mac, or
`sha256sum -c SpeedBreaker-steamos-x86_64.tar.xz.sha256` on SteamOS (with your file's name): it
should say OK. What changed in each release is in [CHANGELOG.md](CHANGELOG.md).
### System requirements
- **Everyone:** a disc image of your own **USA (NTSC-U) Xbox 360 disc** (see
[Quick start](#quick-start)), about **7.2 GB** of free space, and a display running at about
**60 Hz**.
- **Steam Deck, Steam Machine and Linux PCs** (the x86-64 SteamOS download): a 64-bit x86 CPU
with AVX2 (x86-64-v3): Intel Haswell (2013) or newer, or AMD Excavator, Zen or newer. A
Vulkan 1.2 graphics card.
- **Steam Frame** (the arm64 SteamOS download): SteamOS on the Steam Frame. The arm64 download
is for the Frame only; it hasn't been tested on any other arm64 Linux device.
- **Mac:** Apple silicon and **macOS 26** or later. The app won't open on older versions of
macOS, and there is no Intel Mac build.
- **iPhone and iPad:** iOS or iPadOS 17 or later, on an A15-class chip or newer (iPhone 13 and
later, M-series iPads), and a controller or keyboard. Devices with less memory than the tested
ones are untested.
## Quick start
1. **Back up your own disc.** SpeedBreaker supports one version of the game: the **USA (NTSC-U,
English) Xbox 360 disc**. The installer takes a full `.iso`, a trimmed XISO, or a folder with
the extracted disc files. No title update is needed. SpeedBreaker doesn't provide game files
or disc images, or link to them.
2. **Get SpeedBreaker.** [Download it](#download) for Steam Deck, Steam Machine and Steam Frame
(SteamOS), Linux or Mac. On iPhone and iPad you build it yourself. Then follow the
[setup for your platform](#setup-by-platform).
3. **Start it and install.** On first run, SpeedBreaker looks for your disc image, checks that
it is the right game and version, then copies and verifies every file. This takes a few
minutes at most. Afterwards you can delete the disc image.
<img src="assets/screenshots/speedbreaker-12-installer-every-file-verified.png" alt="The installer's last page: every file matched the known-good disc" width="600">
4. **Race.** Your saves and settings are kept apart from the game, so they stay when you update
SpeedBreaker or reinstall the game. The settings open with **F1**, **Back + Start** on a
controller, or a **three-finger tap** on iPhone and iPad.
## Setup by platform
### Steam Deck and Steam Machine
1. **Download and unpack** (in Desktop Mode). Download the SteamOS build,
[`SpeedBreaker-steamos-x86_64.tar.xz`][dl-steamos-x86_64]. Make a `Games` folder in your home
folder, move the file there, and choose *Extract archive here*. Or, in Konsole:
```sh
mkdir -p ~/Games && tar -xf ~/Downloads/SpeedBreaker-steamos-x86_64.tar.xz -C ~/Games
```
Keep it on internal storage or on an SD card formatted by SteamOS, and don't use `:`, `;` or
`$` in the folder names. To update later, delete the old download, download the new one and
unpack it over the same folder.
2. **Add it to Steam.** Steam > Games > *Add a Non-Steam Game to My Library* > Browse. Set the
file type to *All Files* and pick `~/Games/SpeedBreaker/speedbreaker.sh`. Leave
*Compatibility* (Proton) off: this is a native Linux game.
3. **Put your disc image in Downloads,** or in the top folder of an SD card. SpeedBreaker also
looks in your home folder, Desktop, Documents, Games, USB drives and EmuDeck's
`Emulation/roms/xbox360`. Game Mode only sees SD cards and USB drives formatted by SteamOS;
for a card formatted on a PC, copy the image into Downloads first.
4. **Install** (back in Game Mode). Start SpeedBreaker from your library. It highlights the image
it found (*Ready to install*; with several, pick yours with Up and Down): press **A**, then
**A** again on *Install*. Use the images it finds rather than *Browse for image...*, which may
not open in Game Mode.
5. **Check the screen settings.**
- **Steam Deck OLED: set the refresh rate to 60 Hz** (Quick Access > Performance). At 90 Hz
the game stutters. On any Deck, leave the Framerate Limit off.
- **Docked, or on a Steam Machine:** in the shortcut's Properties > General > Game
Resolution, choose **Native**.
- The thin bars above and below the picture on the Deck's screen are intended.
**Controls:** the Deck's controls work through Steam Input (default Gamepad layout).
**Settings:** hold **View** (two squares, left of the screen) and press **Menu** (three lines,
right of the screen).
**Uninstall:** delete `~/Games/SpeedBreaker/`, `~/.local/share/speedbreaker/` (this deletes your
saves too) and `~/.cache/speedbreaker/`, and remove the shortcut from Steam.
### Steam Frame
The Steam Frame has its own download. You set it up on the Frame's desktop: open the
**Desktop** app in your Steam library.
1. **Download and unpack.** Download the Steam Frame build,
[`SpeedBreaker-steamos-arm64.tar.xz`][dl-steamos-arm64], into Downloads (not
`SpeedBreaker-steamos-x86_64.tar.xz`: that one is for the Steam Deck, Steam Machine and Linux
PCs). The Frame's file manager can't unpack it, so open **Konsole** and run:
```sh
mkdir -p ~/Games && tar -xf ~/Downloads/SpeedBreaker-steamos-arm64.tar.xz -C ~/Games
```
This makes `~/Games/SpeedBreaker`. Keep it on the Frame's internal storage, and don't use `:`,
`;` or `$` in the folder names. To update later, delete the old download, download the new
one and unpack it over the same folder.
2. **Add it to Steam.** In the file manager, open `~/Games/SpeedBreaker`, right-click
`speedbreaker.sh` and choose *Add to Steam*. Or, in Konsole:
`steamos-add-to-steam ~/Games/SpeedBreaker/speedbreaker.sh`. Leave *Compatibility* (Proton)
off: this is a native Linux game.
3. **Put your disc image in Downloads.** SpeedBreaker also looks in your home folder, Desktop,
Documents and Games.
4. **Install.** Start SpeedBreaker from your Steam library. It highlights the image it found
(*Ready to install*): press **A**, then **A** again on *Install*. Or install
from Konsole: `~/Games/SpeedBreaker/speedbreaker.sh --install ~/Downloads/your-image.iso`
(with your image's name).
SpeedBreaker plays as a big flat screen in the headset. It is not a VR mode.
- **Settings:** press **Menu + View** together.
- **Frame rate:** set **Display > Frame Rate** to **30 fps** for smooth play. In the headset,
the VR compositor shares the graphics chip. The game keeps its full speed at 30.
- **Uninstall:** as on the [Steam Deck](#steam-deck-and-steam-machine).
### Linux
There's no separate desktop-Linux package yet. Try the x86-64 SteamOS download,
[`SpeedBreaker-steamos-x86_64.tar.xz`][dl-steamos-x86_64], which brings its own runtime libraries
but hasn't been tested outside SteamOS, or [build from source](#building-from-source). Unpack it,
put your `.iso` in your home folder, Downloads, Desktop, Documents or Games (or on a USB drive),
and run `speedbreaker.sh`. The game starts in a window; Display > Fullscreen switches to full
screen.
### Mac
1. **Download and install.** Download [`SpeedBreaker-macos.dmg`][dl-macos], open it, and drag
SpeedBreaker to your Applications folder.
2. **Open it.** The app isn't notarized by Apple, so the first time you open it, macOS refuses.
Close that message (don't move the app to the Trash), then go to **System Settings > Privacy &
Security**, scroll down to the message about SpeedBreaker, click **Open Anyway** and confirm.
You only need to do this the first time you open a new download. On current macOS,
Control-click > Open no longer skips this step.
3. **Put your disc image in a `Games` folder in your home folder** (`~/Games`). That's the only
place SpeedBreaker looks by itself, so macOS doesn't have to ask for access to your other
folders. For an image anywhere else, use *Browse for image...*.
4. **Install.** The game goes to `~/Library/Application Support/SpeedBreaker/game`, or to an
external drive if you choose one.
**Settings:** **F1** or **Cmd+,**, or Back + Start on a controller. The game starts in a window;
Display > Fullscreen switches to full screen. SpeedBreaker needs macOS 26 or later.
### iPhone and iPad
There's no download for iPhone and iPad, and SpeedBreaker isn't on the App Store. You build it
yourself with Xcode on a Mac and install it on your own device, signed with your own Apple
developer account: [scripts/ios/README.md](scripts/ios/README.md) walks you through it.
Once SpeedBreaker is on your device:
1. **Get your disc image onto it.** Copy the `.iso` into **Files > On My iPhone (or iPad) >
SpeedBreaker**, where it is found by itself. Or tap **Browse** and pick it from anywhere the
Files app reaches, such as iCloud Drive or a USB drive. A browsed image is read where it is,
not copied first.
2. **Install.** The game goes into the app's own storage, and is left out of iCloud and computer
backups (your saves are still backed up). Afterwards you can delete the disc image.
3. **Connect a controller or a keyboard.** The game itself doesn't use touch. Pair a Bluetooth
controller, such as an Xbox or PlayStation one, or use a Backbone. So far, a Backbone Pro on
the iPad is the one tested.
**Settings:** tap with **three fingers**, or press Back + Start on the controller (View + Menu on
a Backbone). SpeedBreaker runs in landscape, full screen. If you switch away, the game freezes,
and it resumes exactly where it stopped. If 60 fps isn't steady on your iPhone, set Display >
Frame Rate to 30 fps or Auto.
## Controls
SpeedBreaker hands the game an Xbox 360 controller, so a race plays with the game's own Xbox 360
controls. Buttons are matched **by position**: on a PlayStation controller, Cross is A, Circle
is B, Square is X and Triangle is Y.
| Xbox 360 control | Controller | Keyboard |
|---|---|---|
| Left stick | Left stick | A / D steer (the arrow keys also move it) |
| Right trigger | Right trigger | W |
| Left trigger | Left trigger | S |
| A / B / X / Y | Bottom / right / left / top face button | Space / Backspace / Left Shift / Left Ctrl |
| LB / RB | Shoulder buttons | Q / E |
| D-pad | D-pad | Arrow keys |
| Start | Start / Menu / Options | Enter |
| Back | Back / View / Create | Esc |
| Right stick, stick clicks | Same on the controller | C (right stick click) |
- One player, one controller: the first one connected is used, and you can plug one in at any
time. The keyboard always works as well.
- Because Back + Start opens the settings, the game sees Back a moment late: a tap arrives when
you let go, a hold after a quarter of a second.
- There is no button remapping and no mouse control in game.
**To open the settings:**
| Device | Open or close the settings |
|---|---|
| Keyboard | **F1** (on a Mac also **Cmd+,**) |
| Xbox-style controller | **Back + Start** (View + Menu) together |
| Steam Deck | Hold **View**, press **Menu** |
| Steam Frame | **Menu + View** together |
| iPhone and iPad touchscreen | **Three-finger tap** |
## Settings
The game pauses while the settings are open. The D-pad (or arrow keys) chooses and changes a row,
LB and RB (or Q and E) turn pages, and B (or Esc) closes the menu. On a touchscreen, tap and
drag. The mouse works too. Every change applies at once and is saved when you close the menu.
Each page has a *Reset page* button.
<details>
<summary><b>All settings and their defaults</b></summary>
**Display**
| Option | Choices | Default |
|---|---|---|
| Fullscreen | On / Off | Off: a 1280x720 window you can resize. Always on in SteamOS Game Mode. Not shown on iPhone and iPad, which are always full screen. |
| V-Sync | On / Off | On. Off can tear, but shows frames sooner. |
| Frame Rate | Auto / 60 fps / 30 fps | 60 fps on most devices. 30 fps keeps the game at full speed. Auto runs at 60, or at a steady 30 while the device is hot or can't hold 60. |
| Screen Resolution | Full / 75% | Full. iPhone and iPad only: 75% is slightly softer and saves graphics work. |
| Aspect Ratio | Fill Screen / 16:9 | Fill Screen: wider screens get a wider view, 4:3 and 3:2 screens a taller one. 16:9 always shows the classic picture with bars. |
**Graphics**
| Option | Choices | Default |
|---|---|---|
| Anti-Aliasing | On / Off | On |
| Upscaling | Bicubic / Bilinear | Bicubic |
| Sharpening | 0.00 to 1.00 | 0.30 (0 turns it off) |
| Anisotropic Filtering | Auto / Off / 2x / 4x / 8x / 16x / Original | Auto: 16x on dedicated graphics cards and M-series iPads, 4x elsewhere. Original uses each texture's own setting, as on the 360. |
| Mipmaps | On / Off | On, so distant textures don't shimmer |
| Internal Resolution | Auto / 1x (720p) / 2x (1440p) / 3x (2160p) | Auto: enough to cover your screen, within what the device can afford. On iPhone and iPad, Auto always picks 1x. |
**Audio:** Master Volume (100%), Mute (Off), and Mute in Background (Off), which silences the
game while its window isn't in front.
**Input:** Stick Deadzone (8%; raise it if the car steers by itself), Trigger Deadzone (2%), and
Vibration (On).
**Advanced:** Asynchronous Shaders (On: an object is skipped for a frame or two while its shader
compiles, instead of the game stuttering), Performance Overlay (Off: fps, frame times, and CPU
and GPU time per frame), and **Save Bug Report**. The page also shows which build you're running.
</details>
## Tips for smooth play
- **Run your display at 60 Hz.** On a Steam Deck OLED, set the refresh rate to 60 Hz and leave
Steam's Framerate Limit off.
- **Start with Auto.** If busy scenes drop below 60, set Internal Resolution one step lower, or
let Frame Rate *Auto* fall back to a steady 30.
- **30 fps is a real option.** It's a steady 30 at full game speed, and the smooth choice in the
Steam Frame headset.
- **On iPhone and iPad,** keep Internal Resolution at Auto (1x). On the tested iPad Pro (M2), 2x
is too slow for races. Screen Resolution 75% saves more.
- **Docked Deck or Steam Machine:** set the shortcut's Game Resolution to Native.
- **See what's happening:** Settings > Advanced > Performance Overlay.
## FAQ and troubleshooting
**Does SpeedBreaker include the game?**
No. No game files are included or distributed. You install the game from your own backup of
your own disc, and SpeedBreaker checks it file by file.
**Which version of the game do I need?**
The Xbox 360 **USA release (NTSC-U, English)**. The installer checks your image and tells you if
it's a different version. PAL and Japanese discs and re-releases haven't been tested, so expect
the installer to refuse them. Games on Demand packages aren't supported, and no title update is
used.
**Does it work with the PC version of the game?**
No. SpeedBreaker is built from the Xbox 360 version and installs only from the Xbox 360 USA disc.
PC copies of *Most Wanted*, including the Black Edition, aren't supported.
**Is this an emulator?**
No. It's a static recompilation: the game's Xbox 360 code was translated ahead of time into a
native app, so that code runs directly instead of being emulated. SpeedBreaker supplies what the
game expects from the Xbox 360: its system software, graphics, audio and controllers.
**Can it run above 60 fps?**
No. 60 fps is the game's own cap.
**Can I bring my Xbox 360 saves?**
No. Xbox 360 saves can't be imported, so your career starts fresh.
**The installer refused my image. Why?**
It always says what to do. The usual causes:
- *Another game, or an unsupported version:* it isn't the USA disc.
- *Truncated image:* most often a copy cut off at 4 GB by a FAT32 drive. Copy it again from your
own backup, onto a drive that can hold files over 4 GB.
- *Not a disc image,* a Games on Demand package, or a video-only DVD image.
- *Missing or corrupt files:* make a new backup from your disc.
**My Mac says SpeedBreaker can't be opened.** Use **Open Anyway** in System Settings > Privacy &
Security (see [Mac setup](#mac)).
**The game stutters on my Steam Deck OLED.** Set the screen to 60 Hz (see
[Steam Deck setup](#steam-deck-and-steam-machine)).
**The picture is small or stretched on my TV or monitor in Game Mode.** Set the shortcut's
Properties > General > Game Resolution to Native.
**My Deck shows bars above and below the picture.** That's intended: the game is 16:9 and the
Deck's screen is 16:10.
**My SD card or USB drive doesn't show up in Game Mode.** Game Mode only sees drives formatted by
SteamOS. Copy the image into Downloads from Desktop Mode.
**The car steers by itself.** Raise Settings > Input > Stick Deadzone.
**The game ignores my touches on iPhone or iPad.** The game needs a controller or a keyboard.
Touch only works in SpeedBreaker's own screens; a three-finger tap opens the settings.
**Where are my saves, settings and logs?**
| | Linux and SteamOS | Mac | iPhone and iPad |
|---|---|---|---|
| Saves, settings, logs | `~/.local/share/speedbreaker/` | `~/Library/Application Support/SpeedBreaker/` | Inside the app (not shown in the Files app) |
| Installed game | `game/` in that folder, or `speedbreaker/game` on a drive | `game/` in that folder, or `SpeedBreaker/game` on a drive | Inside the app |
| Shader caches (safe to delete) | `~/.cache/speedbreaker/` | `~/Library/Caches/SpeedBreaker/` | Inside the app |
Deleting that folder, or deleting the iPhone or iPad app, deletes your saves.
### Reporting a bug
1. Open the settings, go to **Advanced**, and choose **Save Bug Report**.
2. A notice says where the zip file went: your **Desktop** (on SteamOS, pick it up in Desktop
Mode, or in the Desktop app on a Steam Frame), the **SpeedBreaker** folder in the Files app on
iPhone and iPad, or the data folder above when there is no Desktop. Its name says which
version you were running, for example `speedbreaker-bug-report-v0.1.0-<date>_<time>.zip`.
3. Open an [issue][issues], attach the zip, and say what you were doing, what you expected, what
happened, and roughly how long into the session it happened.
The zip holds recent logs, any crash and hang reports, your settings, a summary of your system
and a screenshot of the last game frame. Paths in your home folder show as `~`. If the game
crashed, start it again and save a report: the crash report is included. **Never attach game
files or disc images to an issue.**
## What's next
- **Windows:** coming soon.
- **Android:** coming soon.
**One more thing… SpeedBreaker: Carbon, coming soon.** It's planned, not available yet.
There are no dates and no promises: it's ready when it's ready.
## For developers
### How it works
SpeedBreaker is a **static recompilation**. [XenonRecomp](https://github.com/hedge-dev/XenonRecomp)
translates the game's PowerPC code into C++ ahead of time, and that C++ is compiled into a
native program for x86-64 or arm64. A runtime written for this project stands in for the Xbox
360: its kernel and system software, its GPU, its XMA audio and its controllers.
- **Graphics:** a Vulkan renderer that translates the Xenos GPU's commands and shaders. On Mac,
iPhone and iPad it runs on Metal through MoltenVK.
- **Platform layer:** SDL3 for windows, input and audio. The installer, the settings menu and the
overlay are drawn with Dear ImGui.
- **Audio:** XMA decoding done the way the hardware does it, frame by frame, with a patched,
decoder-only FFmpeg.
- **Resolution:** the 3D scene renders at 1x, 2x or 3x; the bloom and haze stay at the original
resolution, so the game keeps its colour grade.
- **Frame pacing:** the game's clock locks to the display's measured refresh (58.5 to 61.5 Hz),
one game step per refresh. 30 fps runs two game steps per frame.
- **Sun glare:** the game measures the sun with occlusion queries; SpeedBreaker counts them on
the GPU.
The game's data (cars, city, audio, movies) comes from the player's disc. SpeedBreaker contains
no game data or assets.
### Command-line installer
The installer also runs from a terminal. On SteamOS, run it through `speedbreaker.sh`; on a Mac,
the program is `SpeedBreaker.app/Contents/MacOS/SpeedBreaker`.
```sh
SpeedBreaker --install <image or folder> [--dest <dir>] # install
SpeedBreaker --verify <image or folder> # check every file without copying
SpeedBreaker --find-images # list the disc images it can find
SpeedBreaker --where # show where the game is installed
```
### Building from source
The repository contains no game code: the build recompiles your own `default.xex`, from the USA
disc. The generated code (`ppc/`), the game files (`game/`) and the toolchain (`tools/`) are
ignored by git. **Never commit or share them.**
The supported executable is 3,936,256 bytes, with SHA-256
`aebdf3c14d1e4e4ed2c130d94bc1a0e97bd6d5bf39bdfdef33c3909c9cc4ba4c`.
#### 1. Dependencies
- **Mac:** Xcode's command-line tools, and from Homebrew: `brew install llvm lld cmake ninja sdl3
molten-vk vulkan-loader vulkan-headers glslang spirv-tools`
- **SteamOS:** build inside an Arch Linux distrobox (podman and distrobox ship with SteamOS). The
development launcher, `scripts/steamos/speedbreaker.sh`, expects the container to be called
`nfsmw-build`:
```sh
distrobox create --yes --name nfsmw-build --image docker.io/library/archlinux:latest \
--additional-packages "clang lld cmake ninja git python sdl3 vulkan-headers \
vulkan-icd-loader vulkan-tools glslang spirv-tools mesa vulkan-radeon pkgconf \
make diffutils patch curl nasm libpulse pipewire pipewire-pulse alsa-lib libdecor"
distrobox enter nfsmw-build
```
- **Other Linux distributions:** your distribution's equivalents of the package list above
(Clang 18 or newer). Untested so far. The GPU needs Vulkan 1.2 and BC (DXT) texture support.
- `gh`, logged in, is optional: it fetches Xenia's instruction tests.
#### 2. Toolchain, your game files, recompilation
From the repository root:
```sh
scripts/setup_tools.sh # XenonRecomp + SpeedBreaker's patches, extract-xiso, FFmpeg's XMA decoder
# Extract your own disc image into game/files and check the executable.
mkdir -p game/files
tools/extract-xiso/build/extract-xiso -d game/files -x /path/to/your-backup.iso
shasum -a 256 game/files/default.xex
# must print aebdf3c14d1e4e4ed2c130d94bc1a0e97bd6d5bf39bdfdef33c3909c9cc4ba4c
# Translate the game's code into C++ (writes ppc/).
mkdir -p ppc
tools/XenonRecomp/build/XenonRecomp/XenonRecomp config/nfsmw.toml tools/XenonRecomp/XenonUtils/ppc_context.h
```
#### 3. Build
```sh
# Linux and SteamOS (on SteamOS, inside the container)
cmake -S . -B build/main -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
# Mac (Homebrew's LLVM)
cmake -S . -B build/main -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=/opt/homebrew/opt/llvm/bin/clang \
-DCMAKE_CXX_COMPILER=/opt/homebrew/opt/llvm/bin/clang++
ninja -C build/main SpeedBreaker
```
Run `build/main/runtime/SpeedBreaker` from the repository root. With the whole disc extracted in
`game/files`, it plays from there. Otherwise the first-run installer asks for your disc image.
#### 4. Package it (optional)
- **Mac app:** `python3 scripts/macos/make_app.py` makes `build/app/SpeedBreaker.app`, with its
libraries, MoltenVK and the license notices inside. Add `--dmg` for
`build/app/SpeedBreaker-macos.dmg` and its `.sha256`, laid out like the download on Releases.
- **SteamOS bundle (x86-64):** inside the container, `scripts/steamos/make_bundle.sh` makes
`build/bundle/SpeedBreaker-steamos-x86_64.tar.xz` and its `.sha256` (plus a copy named by the
version, `SpeedBreaker-<version>-steamos-x86_64.tar.xz`). Install it as described in
[Steam Deck setup](#steam-deck-and-steam-machine).
- **Steam Frame bundle (arm64):** the Steam Frame download on Releases is built on a Frame, and
so is your own: the Frame's SteamOS ships clang, CMake, Ninja, glslang and the Vulkan headers,
so no container is needed. Build an SDL3 release into `~/deps/sdl3` (with
`-DSDL_X11_XSCRNSAVER=OFF`), and do step 2 on the Frame (another machine's FFmpeg build won't
link there). Then:
```sh
cmake -S . -B build/main -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ -DSDL3_DIR=$HOME/deps/sdl3/lib/cmake/SDL3 \
-DCMAKE_SKIP_BUILD_RPATH=ON
ninja -C build/main
scripts/steamos/make_bundle.sh build/main build/bundle
```
This makes `build/bundle/SpeedBreaker-steamos-arm64.tar.xz` and its `.sha256` (plus a copy
named by the version). `-DCMAKE_SKIP_BUILD_RPATH=ON` keeps your `~/deps` folder out of the
program: the bundle brings SDL3 in its own `lib/`. Install it as described in [Steam Frame setup](#steam-frame).
- **iPhone and iPad:** `scripts/ios/build_deps.sh` builds the iOS libraries, and
`scripts/ios/build.sh` builds, signs and installs the app. The steps, and signing with your own
Apple developer account, are in [scripts/ios/README.md](scripts/ios/README.md).
#### Tests
The unit tests build with `-DSB_TESTS=ON` added to the `cmake` line, and
`ctest --test-dir build/main` runs them. `tests/run_instr_tests.sh` checks the instructions
SpeedBreaker's patches add to XenonRecomp. It needs LLVM's `llvm-mc` and `llvm-objdump` and
`ld.lld`: on a Mac, Homebrew's `llvm` and `lld`; elsewhere, your distribution's (on Arch, the
`llvm` and `lld` packages; the container above has only `lld`). `LLVM=<bin dir>` and
`LLD=<ld.lld>` pick others. So far it has been run on a Mac only. None of the tests needs game
files; `write_watch_test` needs `ppc/`.
## Credits
SpeedBreaker is made by **project(u)**. It stands on the shoulders of a lot of open-source work.
Thank you to the **hedge-dev** team, for XenonRecomp and for the approach Unleashed Recompiled
pioneered that this project follows, and to the **Xenia** project, for a decade of Xbox 360
research.
| Project | What SpeedBreaker uses it for | License |
|---|---|---|
| [XenonRecomp](https://github.com/hedge-dev/XenonRecomp) (hedge-dev and contributors) | The PowerPC-to-C++ recompiler. Its XenonUtils (XEX loading) is built into the runtime. | MIT |
| [Unleashed Recompiled](https://github.com/hedge-dev/UnleashedRecomp) (hedge-dev and contributors) | The reference runtime. Parts of the kernel, XAM, memory and threading code are adapted from it. | GPL-3.0-or-later |
| [Xenia](https://github.com/xenia-project/xenia) (Ben Vanik and Xenia contributors) | GPU register, microcode and Xenos headers are ported from it, and its kernel, XMA and GPU behaviour is the reference throughout. | BSD-3-Clause |
| freedreno (Rob Clark) | Code inside the ported microcode header | MIT |
| [FFmpeg](https://ffmpeg.org/) 7.1.1 | XMA decoding: a decoder-only static build with a SpeedBreaker patch | LGPL-2.1-or-later |
| [SDL3](https://www.libsdl.org/) | Windows, input, audio, app lifecycle | zlib |
| [Dear ImGui](https://github.com/ocornut/imgui) (Omar Cornut) | The installer and settings UI | MIT |
| [Roboto](https://fonts.google.com/specimen/Roboto) (Google) | The UI font | Apache-2.0 |
| [o1heap](https://github.com/pavel-kirienko/o1heap) (Pavel Kirienko) | The guest heap allocator | MIT |
| [glslang](https://github.com/KhronosGroup/glslang) and [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools) (Khronos) | Shader compilation at runtime | BSD-3-Clause and others; Apache-2.0 |
| [MoltenVK](https://github.com/KhronosGroup/MoltenVK) and the [Vulkan Loader](https://github.com/KhronosGroup/Vulkan-Loader) (Khronos) | Vulkan on Mac, iPhone and iPad | Apache-2.0 |
| [toml++](https://github.com/marzer/tomlplusplus) (Mark Gillard) | `settings.toml` | MIT |
| [SIMDe](https://github.com/simd-everywhere/simde) (Evan Nemerson and contributors) | SIMD in the recompiled code | MIT |
| [libmspack](https://github.com/kyz/libmspack) (Stuart Caie) | LZX decompression | LGPL-2.1 |
| [tiny-AES-c](https://github.com/kokke/tiny-AES-c) | XEX decryption | Unlicense |
| [TinySHA1](https://github.com/mohaps/TinySHA1) (Saurav Mohapatra) | SHA-1, via XenonUtils | ISC-style |
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | Developer tool for unpacking disc images (not shipped) | Its own license |
The x86-64 SteamOS download also carries glibc and libstdc++ (LGPL-2.1+; GPL-3.0 with the GCC
Runtime Library Exception); the Steam Frame download uses the system's. Full license texts are in
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSES/](LICENSES/), and every download
carries them too: in `licenses/` in the SteamOS downloads, and in
`SpeedBreaker.app/Contents/Resources/licenses` in the Mac app, with each bundled library's own
license. The source code of the LGPL and GPL parts in the downloads (FFmpeg, libmspack, and the
x86-64 download's glibc and GCC runtime) is published next to the downloads of each release, as
`SpeedBreaker-<version>-third-party-source.tar.xz`.
## License
SpeedBreaker is free software: you can redistribute it and/or modify it under the terms of the
**GNU General Public License** as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version (GPL-3.0-or-later). See [COPYING](COPYING)
and [NOTICE](NOTICE). Third-party components keep their own licenses, listed above.
Copyright (C) 2026 project(u) and SpeedBreaker contributors.
## Legal notice
SpeedBreaker is an unofficial, fan-made project by project(u). It is **not affiliated with,
endorsed by or sponsored by Electronic Arts Inc., the Need for Speed brand or Valve
Corporation.**
"Need for Speed", "Most Wanted" and "Carbon" are trademarks of Electronic Arts Inc. Steam,
Steam Deck, Steam Machine, Steam Frame, SteamOS and their logos and icons are trademarks of
Valve Corporation. All other trademarks, including Xbox, Xbox 360, PlayStation, Apple, Mac,
macOS, iPhone, iPad, iCloud, Xcode, App Store, Backbone, Vulkan, Linux, Windows and Android,
belong to their respective owners. They are used here only to describe what SpeedBreaker works
with. The game is named only to say which game SpeedBreaker runs; no logos, box art or key art
of the game are used. The game content visible in screenshots belongs to its owners.
**No game files are included or distributed.** SpeedBreaker contains no game data or assets: no
textures, models, audio, movies or disc files. To play, you need your own legally obtained
backup (a disc image) of your own Xbox 360 disc of *Need for Speed: Most Wanted* (2005). The
in-app installer verifies it and installs it.
SpeedBreaker is provided "as is", without warranty of any kind, as set out in the GNU General
Public License.
[releases]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest
[dl-steamos-x86_64]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest/download/SpeedBreaker-steamos-x86_64.tar.xz
[dl-steamos-arm64]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest/download/SpeedBreaker-steamos-arm64.tar.xz
[dl-macos]: https://github.com/SpeedBreakerProject/speedbreaker/releases/latest/download/SpeedBreaker-macos.dmg
[issues]: https://github.com/SpeedBreakerProject/speedbreaker/issues
+72
View File
@@ -0,0 +1,72 @@
# Third-party notices
SpeedBreaker is Copyright (C) 2026 project(u) and SpeedBreaker contributors, and is free software
under the GNU General Public License, version 3 or (at your option) any later version
(GPL-3.0-or-later; [`COPYING`](COPYING), [`NOTICE`](NOTICE)). It is built from, and its downloads
include, software by others under their own licenses, listed here. The license texts are in
[`LICENSES/`](LICENSES).
The downloads carry these notices too: the Mac app in `SpeedBreaker.app/Contents/Resources/licenses`,
each SteamOS download in its `licenses/` folder (with the license of every library it bundles,
collected when the bundle is made).
## Compiled into SpeedBreaker (every platform)
| Project | What it does here | License | Text |
|---|---|---|---|
| **Unleashed Recompiled**: hedge-dev and contributors | Parts of the kernel, XAM, memory, heap and threading code are adapted from it (each such file says so at the top). | GPL-3.0-or-later | [`LICENSES/GPL-3.0.txt`](LICENSES/GPL-3.0.txt) |
| **Xenia**: Copyright Ben Vanik and Xenia contributors | The Xenos GPU register, microcode and format headers are ported from it (`runtime/gpu/xenos/`), as is the NT status table (`runtime/kernel/imports_error.cpp`). Its kernel, XMA and GPU behaviour is the reference throughout. | BSD-3-Clause | [`LICENSES/Xenia-BSD-3-Clause.txt`](LICENSES/Xenia-BSD-3-Clause.txt) |
| **freedreno**: Copyright (c) 2012 Rob Clark | Shader instruction formats, through Xenia's `ucode.h`. | MIT | [`LICENSES/freedreno-MIT.txt`](LICENSES/freedreno-MIT.txt) |
| **XenonRecomp** (XenonUtils): Copyright (c) 2025 hedge-dev and contributors | Recompiles the game's PowerPC code into C++ (with SpeedBreaker's patches, `patches/xenonrecomp/`); its XEX loader is compiled into the runtime. | MIT | [`LICENSES/XenonRecomp-MIT.md`](LICENSES/XenonRecomp-MIT.md) |
| **libmspack**: (C) 2003-2023 Stuart Caie | LZX decompression of the XEX (`lzxd.c`, through XenonRecomp). | LGPL-2.1 | [`LICENSES/libmspack-LGPL-2.1.txt`](LICENSES/libmspack-LGPL-2.1.txt) |
| **tiny-AES-c** | XEX decryption (through XenonRecomp). | Unlicense | [`LICENSES/tiny-AES-c-Unlicense.txt`](LICENSES/tiny-AES-c-Unlicense.txt) |
| **TinySHA1**: Copyright (c) 2012-22 Saurav Mohapatra | SHA-1, in XenonRecomp's XEX patcher. | ISC-style | [`LICENSES/TinySHA1-ISC.txt`](LICENSES/TinySHA1-ISC.txt) |
| **toml++**: Copyright (c) Mark Gillard | Reads and writes `settings.toml` (through XenonRecomp). | MIT | [`LICENSES/tomlplusplus-MIT.txt`](LICENSES/tomlplusplus-MIT.txt) |
| **SIMDe**: Copyright (c) 2017 Evan Nemerson and contributors | Vector instructions in the recompiled code (through XenonRecomp). | MIT | [`LICENSES/SIMDe-MIT.txt`](LICENSES/SIMDe-MIT.txt) |
| **Dear ImGui** 1.92.9: Copyright (c) 2014-2026 Omar Cornut | The installer, the settings menu and the overlays (`runtime/thirdparty/imgui`). | MIT | [`LICENSES/DearImGui-MIT.txt`](LICENSES/DearImGui-MIT.txt) |
| **stb_truetype, stb_rect_pack, stb_textedit**: Copyright (c) 2017 Sean Barrett | Font rasterising and text editing, inside Dear ImGui. | MIT (or public domain) | [`LICENSES/stb-MIT.txt`](LICENSES/stb-MIT.txt) |
| **Roboto** (Medium): Copyright 2011 Google Inc. | The menus' font, embedded in the program. Roboto is a trademark of Google. | Apache-2.0 | [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt) |
| **o1heap**: Copyright (c) 2020 Pavel Kirienko | The game's heap allocator (`runtime/thirdparty/o1heap`). | MIT | [`LICENSES/o1heap-MIT.txt`](LICENSES/o1heap-MIT.txt) |
| **FFmpeg** 7.1.1: the FFmpeg developers | The XMA1, XMA2 and WMA Pro audio decoders, linked statically. Built without any GPL parts, with one SpeedBreaker change: [`patches/ffmpeg/0001-xma-single-stream-no-holdback.patch`](patches/ffmpeg/0001-xma-single-stream-no-holdback.patch) (no sample hold-back for single-stream XMA, which the game's audio mixer depends on). Source: `https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz` plus that patch; [`scripts/build_ffmpeg_xma.sh`](scripts/build_ffmpeg_xma.sh) builds it. | LGPL-2.1-or-later | [`LICENSES/FFmpeg-LGPL-2.1.txt`](LICENSES/FFmpeg-LGPL-2.1.txt) |
## Libraries in the downloads
| Project | Where | License | Text |
|---|---|---|---|
| **SDL3**: Copyright (C) 1997-2026 Sam Lantinga | Windows, input, audio and the app's lifecycle. In the Mac app and both SteamOS downloads. | zlib | [`LICENSES/SDL3-Zlib.txt`](LICENSES/SDL3-Zlib.txt) |
| **glslang**: The Khronos Group Inc. and others | Compiles the game's shaders while it runs. In the Mac app and both SteamOS downloads. | BSD-3-Clause and others (its own list) | [`LICENSES/glslang.txt`](LICENSES/glslang.txt) |
| **SPIRV-Tools**: The Khronos Group Inc. and others | Used by glslang. In the Mac app and both SteamOS downloads. | Apache-2.0 | [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt) |
| **MoltenVK** (with SPIRV-Cross): Copyright (c) 2015-2026 The Brenwill Workshop Ltd., The Khronos Group Inc. | Vulkan on Apple's Metal. In the Mac app (and an iPhone or iPad build). | Apache-2.0 | [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt) |
| **Vulkan Loader**: The Khronos Group Inc., Valve Corporation, LunarG, Inc. | In the Mac app (SteamOS has its own). | Apache-2.0 | [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt) |
| **GNU C Library** (glibc) and **GCC's runtime libraries** (libstdc++, libgcc_s) | In the SteamOS x86-64 download only, from the Arch Linux packages it was built with (the download's `licenses/README.txt` names each package and version). The Steam Frame download uses SteamOS's own. | glibc: LGPL-2.1-or-later. GCC's runtime: GPL-3.0 with the GCC Runtime Library Exception | in the download's `licenses/` |
Every SteamOS download's `licenses/` folder also holds the license files of the exact packages its
libraries came from.
## Source code of the LGPL and GPL parts
FFmpeg and libmspack (LGPL-2.1) are compiled into every download, and the SteamOS x86-64 download
carries the GNU C Library (LGPL-2.1) and GCC's runtime libraries (GPL-3.0 with the GCC Runtime
Library Exception). Their complete source code, at the versions in the downloads, is published
next to the downloads of each release on the [Releases](https://github.com/SpeedBreakerProject/speedbreaker/releases)
page, as `SpeedBreaker-<version>-third-party-source.tar.xz`:
- FFmpeg 7.1.1 as released (`ffmpeg-7.1.1.tar.xz`), SpeedBreaker's patch and the build script
with its configure options;
- libmspack, as XenonRecomp's pinned submodule has it;
- the GNU C Library and GCC, at the Arch Linux package versions the x86-64 download's
`licenses/README.txt` names, with Arch's build files and patches for them.
If it is ever missing, ask through the project's [issue tracker](https://github.com/SpeedBreakerProject/speedbreaker/issues):
for at least three years after a release, its copy is provided at no charge.
## Tools used to build it, not shipped
- **XenonRecomp** (above) and **XenosRecomp**: hedge-dev and contributors, MIT.
- **extract-xiso**: for developers unpacking their own disc image.
- **Xenia's PowerPC instruction tests**: the recompiler's instruction tests (`tests/`).
## Thanks
The hedge-dev team, for XenonRecomp and the approach of Unleashed Recompiled that SpeedBreaker
follows, and the Xenia project, for years of Xbox 360 research.
Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+256
View File
@@ -0,0 +1,256 @@
# XenonRecomp config for NFS: Most Wanted (Xbox 360, USA)
# default.xex SHA-256 aebdf3c14d1e4e4ed2c130d94bc1a0e97bd6d5bf39bdfdef33c3909c9cc4ba4c
# Paths are relative to this file.
[main]
file_path = "../game/files/default.xex"
out_directory_path = "../ppc"
switch_table_file_path = "switch_tables.toml"
# Optimizations stay off until the recompilation runs correctly.
skip_lr = false
skip_msr = false
ctr_as_local = false
xer_as_local = false
reserved_as_local = false
cr_as_local = false
non_argument_as_local = false
non_volatile_as_local = false
# Found by survey/xexsurvey.cpp byte patterns (one hit each). The layout
# matches Sonic Unleashed's CRT exactly: restgpr = savegpr + 0x50,
# vmx_64 = vmx_14 + 0x94.
restgprlr_14_address = 0x826BDB40
savegprlr_14_address = 0x826BDAF0
restfpr_14_address = 0x826BEAFC
savefpr_14_address = 0x826BEAB0
restvmx_14_address = 0x826BE818
savevmx_14_address = 0x826BE580
restvmx_64_address = 0x826BE8AC
savevmx_64_address = 0x826BE614
# longjmp: restores r14-r31 from the jmp_buf and calls RtlUnwind.
# setjmp: hook check (lis/lwz/bnectr), then mflr/mfcr and saves into r3.
longjmp_address = 0x826BCD30
setjmp_address = 0x826BF180
invalid_instructions = [
{ data = 0x00000000, size = 4 }, # Padding
]
# --- generated by survey/find_entry_points.py --write ---
# Secondary entry points: code addresses the game calls through pointers
# that lie inside another function. Do not edit by hand; rerun.
secondary_entries = [
0x821E6378, # referenced from code; leaf code
0x826302D4, # referenced from data; in .pdata fn 0x82630180
0x826BC5CC, # referenced from code; in .pdata fn 0x826BC4A0
0x826C5278, # referenced from data; in .pdata fn 0x826C51D0
0x826C54CC, # referenced from data; in .pdata fn 0x826C53D0
0x826CB99C, # referenced from data; in .pdata fn 0x826CB8D8
0x826E0EC0, # referenced from code; leaf code
]
# --- end find_entry_points ---
# --- generated by survey/leaf_switch_bounds.py --write ---
# Leaf functions (no .pdata) with jump tables: start = latest entry (bl target,
# data-referenced address, or after padding); end = branch-closure extent;
# overlaps merged. Plus MANUAL entries. Do not edit by hand; rerun.
functions = [
{ address = 0x8215D480, size = 0x88 },
{ address = 0x8217D928, size = 0x150 },
{ address = 0x8217EF08, size = 0x5C },
{ address = 0x8217EF68, size = 0x98 },
{ address = 0x8217F2D8, size = 0xB4 },
{ address = 0x821AB578, size = 0xBC },
{ address = 0x821B6388, size = 0xF4 },
{ address = 0x821B7928, size = 0x1CC },
{ address = 0x821BE740, size = 0x6C },
{ address = 0x821C2BE0, size = 0x80 },
{ address = 0x821CC130, size = 0x58 },
{ address = 0x821D0910, size = 0x35C },
{ address = 0x821E48E8, size = 0xA4 },
{ address = 0x821F2C08, size = 0x7C },
{ address = 0x821F6360, size = 0x64 },
{ address = 0x821FA940, size = 0x128 },
{ address = 0x82225610, size = 0x120 },
{ address = 0x82225A80, size = 0x124 },
{ address = 0x8223E470, size = 0x1D0 },
{ address = 0x82244068, size = 0x58 },
{ address = 0x8224B6C8, size = 0x108 },
{ address = 0x8224DE58, size = 0xD8 },
{ address = 0x822530A8, size = 0x1A0 },
{ address = 0x822585B0, size = 0xE8 },
{ address = 0x82281648, size = 0x84 },
{ address = 0x82281F60, size = 0x52C },
{ address = 0x82285660, size = 0x84 },
{ address = 0x822856E8, size = 0x6C },
{ address = 0x822A6B70, size = 0x80 },
{ address = 0x822ABC68, size = 0x104 },
{ address = 0x822BD9D0, size = 0x70 },
{ address = 0x822C2068, size = 0xA8 },
{ address = 0x822C5AA0, size = 0x9C },
{ address = 0x822C5B40, size = 0xA8 },
{ address = 0x822C7720, size = 0x68 },
{ address = 0x822C8630, size = 0x74 },
{ address = 0x822C89F8, size = 0x44 },
{ address = 0x822CDEB8, size = 0x26C },
{ address = 0x822D3B90, size = 0xDC },
{ address = 0x822D9A20, size = 0x12C },
{ address = 0x822E3D68, size = 0x304 },
{ address = 0x822E7CE0, size = 0x40 },
{ address = 0x822E7D88, size = 0x164 },
{ address = 0x822EA348, size = 0x248 },
{ address = 0x822EA590, size = 0x7C },
{ address = 0x822EAF48, size = 0x234 },
{ address = 0x822EB180, size = 0x88 },
{ address = 0x822EB390, size = 0x55C },
{ address = 0x822EB8F0, size = 0x470 },
{ address = 0x82301CF8, size = 0x9C },
{ address = 0x82326468, size = 0xA0 },
{ address = 0x82326508, size = 0xA0 },
{ address = 0x82326728, size = 0x40 },
{ address = 0x8235C4A8, size = 0xBC },
{ address = 0x8236D758, size = 0x140 },
{ address = 0x8236D968, size = 0x94 },
{ address = 0x8236EDD0, size = 0x158 },
{ address = 0x8238B490, size = 0x84 },
{ address = 0x8240E1A0, size = 0xC8 },
{ address = 0x8245AF48, size = 0x124 },
{ address = 0x8245F790, size = 0x94 },
{ address = 0x8248C8B0, size = 0x1EC },
{ address = 0x82491200, size = 0x118 },
{ address = 0x824921F8, size = 0x138 },
{ address = 0x824945E8, size = 0xC8 },
{ address = 0x824961C8, size = 0x108 },
{ address = 0x8249C330, size = 0xFC },
{ address = 0x824A64E8, size = 0x20C },
{ address = 0x824A66F8, size = 0x968 },
{ address = 0x824DA128, size = 0xAC },
{ address = 0x824EC690, size = 0x94 },
{ address = 0x824EC728, size = 0x64 },
{ address = 0x824F64A8, size = 0x88 },
{ address = 0x82551E50, size = 0xCC },
{ address = 0x825545B0, size = 0xCC },
{ address = 0x825546B8, size = 0x16C },
{ address = 0x82557950, size = 0xB4 },
{ address = 0x82566DE0, size = 0xDC },
{ address = 0x825670F0, size = 0x114 },
{ address = 0x8256B490, size = 0x134 },
{ address = 0x8256BBA8, size = 0x224 },
{ address = 0x8256BF78, size = 0x17C },
{ address = 0x8256C968, size = 0x54 },
{ address = 0x8256CD98, size = 0x28C },
{ address = 0x8256FCE8, size = 0x190 },
{ address = 0x825726E0, size = 0x98 },
{ address = 0x82572778, size = 0x2A4 },
{ address = 0x82576FD0, size = 0x78 },
{ address = 0x8257DE10, size = 0xD0 },
{ address = 0x8257DEE0, size = 0xCC },
{ address = 0x8257FA70, size = 0x144 },
{ address = 0x82581AF0, size = 0x94 },
{ address = 0x82586B20, size = 0x60 },
{ address = 0x825B19E8, size = 0x74 },
{ address = 0x825B1ED0, size = 0x5C },
{ address = 0x825B1F30, size = 0x128 },
{ address = 0x825D85C0, size = 0xC4 },
{ address = 0x826224A0, size = 0x2E0 },
{ address = 0x826234E8, size = 0x98 },
{ address = 0x826266E0, size = 0x9C },
{ address = 0x82626780, size = 0x84 },
{ address = 0x826DBA30, size = 0xFC },
{ address = 0x826DFBA8, size = 0x11C },
{ address = 0x826DFE08, size = 0xD0 },
{ address = 0x826DFED8, size = 0x4D4 },
{ address = 0x826E0E18, size = 0x40C },
{ address = 0x8270C630, size = 0x228 },
{ address = 0x8271E918, size = 0x108 },
{ address = 0x827201D8, size = 0xD4 },
{ address = 0x827AD018, size = 0x380 },
{ address = 0x827DC240, size = 0x2FC },
{ address = 0x828137F0, size = 0x1D8 },
{ address = 0x8286A980, size = 0x2BC },
{ address = 0x8286B8B8, size = 0x154 },
# manual (see MANUAL in survey/leaf_switch_bounds.py)
{ address = 0x8218C508, size = 0x10 },
{ address = 0x822F33D8, size = 0x28 },
]
# Deferred render-callback queue (0x82909650): lock-free producer/consumer
# across threads with no barriers (see runtime/game/render_queue.cpp).
# Release before each publish (the store of the write pointer), acquire
# before the consumer reads an entry.
[[midasm_hook]]
name = "RenderQueuePublish"
address = 0x823C8274
[[midasm_hook]]
name = "RenderQueuePublish"
address = 0x824446D4
[[midasm_hook]]
name = "RenderQueuePublish"
address = 0x824447D8
[[midasm_hook]]
name = "RenderQueuePublish"
address = 0x82444898
[[midasm_hook]]
name = "RenderQueuePublish"
address = 0x82444BCC
[[midasm_hook]]
name = "RenderQueuePublish"
address = 0x82462E28
[[midasm_hook]]
name = "RenderQueueAcquire"
address = 0x823C82E8
# Ultrawide and Vert+ (runtime/game/ultrawide.cpp): the projection's x scale,
# P[0][0], just before it is stored in the view setup (sub_8243EAC0; r28 = the
# view), and f10, the y scale the next instructions negate into P[1][1].
# (The runtime also keeps the hooks' older x-only signatures, so a ppc/
# generated before f10, f0, f2 and f4 were added still links: Hor+ only.)
[[midasm_hook]]
name = "UltrawideProjection"
address = 0x8243EE18
registers = ["r28", "f12", "f10"]
# ... and the front-end (HUD) matrix's x and y scales (f13, f0), at the branch
# that ends its widescreen case (sub_82440D80).
[[midasm_hook]]
name = "UltrawideFrontEnd"
address = 0x82440E4C
registers = ["f13", "f0"]
# ... and the rear-view mirror's picture, a quad in pre-transformed clip
# coordinates (no matrix), at the branch that ends the widescreen case of its
# draw (sub_8245CDF0): f1 = left edge (0..1 of the screen), f2 = its top or
# bottom edge likewise, f3 = signed width, f4 = height.
[[midasm_hook]]
name = "UltrawideMirror"
address = 0x8245CED8
registers = ["f1", "f2", "f3", "f4"]
# Game time in whole vblanks (runtime/game/frame_time.cpp): the frame's
# measured time (milliseconds, 16.16 fixed point) as the main loop
# (sub_823B0008) passes it to the frame (sub_823AFE20) ...
[[midasm_hook]]
name = "GameFrameTime"
address = 0x823B0148
registers = ["r3"]
# ... the world stepper's elapsed time in steps, before it joins the
# remainder (sub_823A2348, `fadds f0,f10,f9`) ...
[[midasm_hook]]
name = "WorldStepTime"
address = 0x823A23E8
registers = ["f10"]
# ... and the steps it runs this frame, counted for the [perf] line.
[[midasm_hook]]
name = "WorldSteps"
address = 0x823A2424
registers = ["r30"]
File diff suppressed because it is too large. Load diff
+645
View File
@@ -0,0 +1,645 @@
# bctr 0x821360cc in 0x82135f98: absolute table at 0x821360d0, index r31, 4 entries (no bounds check)
[[switch]]
base = 0x821360C0
r = 31
labels = [
0x821360E0,
0x821360E0,
0x821362E0,
0x821363F4,
]
# bctr 0x821e4918 in 0x821e48e4: absolute table at 0x821e491c, index r5, 5 entries (no bounds check)
[[switch]]
base = 0x821E490C
r = 5
labels = [
0x821E4930,
0x821E493C,
0x821E4948,
0x821E4954,
0x821E4978,
]
# bctr 0x821f2c1c in 0x821f2c04: absolute table at 0x821f2c20, index r3, 9 entries (no bounds check)
[[switch]]
base = 0x821F2C10
r = 3
labels = [
0x821F2C44,
0x821F2C4C,
0x821F2C54,
0x821F2C5C,
0x821F2C64,
0x821F2C6C,
0x821F2C6C,
0x821F2C74,
0x821F2C7C,
]
# bctr 0x821fa954 in 0x821fa940: absolute table at 0x821fa958, index r6, 12 entries (no bounds check)
[[switch]]
base = 0x821FA948
r = 6
labels = [
0x821FA9A0,
0x821FA9BC,
0x821FA9F4,
0x821FAA10,
0x821FAA28,
0x821FA988,
0x821FAA28,
0x821FAA30,
0x821FAA30,
0x821FA988,
0x821FAA4C,
0x821FA9D8,
]
# bctr 0x82211228 in 0x82211208: absolute table at 0x8221122c, index r5, 10 entries (no bounds check)
[[switch]]
base = 0x8221121C
r = 5
labels = [
0x82211278,
0x82211420,
0x822112E4,
0x82211440,
0x82211314,
0x82211430,
0x82211380,
0x822113D8,
0x82211254,
0x82211450,
]
# bctr 0x8224407c in 0x8224401c: absolute table at 0x82244080, index r3, 4 entries (no bounds check)
[[switch]]
base = 0x82244070
r = 3
labels = [
0x82244090,
0x8224409C,
0x822440A8,
0x822440B4,
]
# bctr 0x822856fc in 0x82285638: absolute table at 0x82285700, index r3, 6 entries (no bounds check)
[[switch]]
base = 0x822856F0
r = 3
labels = [
0x82285748,
0x82285718,
0x82285724,
0x82285730,
0x82285730,
0x8228573C,
]
# bctr 0x822c5ab4 in 0x822c5a80: absolute table at 0x822c5ab8, index r3, 9 entries (no bounds check)
[[switch]]
base = 0x822C5AA8
r = 3
labels = [
0x822C5ADC,
0x822C5AE8,
0x822C5AF4,
0x822C5B00,
0x822C5B0C,
0x822C5B18,
0x822C5B30,
0x822C5B30,
0x822C5B24,
]
# bctr 0x822c5b58 in 0x822c5a80: absolute table at 0x822c5b5c, index r3, 11 entries (no bounds check)
[[switch]]
base = 0x822C5B4C
r = 3
labels = [
0x822C5B88,
0x822C5B98,
0x822C5BA8,
0x822C5BB8,
0x822C5BD8,
0x822C5BC8,
0x822C5BE0,
0x822C5BE0,
0x822C5BE0,
0x822C5BC8,
0x822C5BD8,
]
# bctr 0x822e9a30 in 0x822e9a00: absolute table at 0x822e9a34, index r5, 56 entries (no bounds check)
[[switch]]
base = 0x822E9A24
r = 5
labels = [
0x822E9B14,
0x822E9BE8,
0x822E9CBC,
0x822E9D10,
0x822E9D18,
0x822E9D20,
0x822E9D28,
0x822E9D30,
0x822E9D38,
0x822E9D40,
0x822E9D48,
0x822E9D50,
0x822E9D58,
0x822E9D60,
0x822E9D68,
0x822E9DB4,
0x822E9D70,
0x822E9DB4,
0x822E9D78,
0x822E9D78,
0x822E9D78,
0x822E9D78,
0x822E9D78,
0x822E9DB4,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D58,
0x822E9D80,
0x822E9D80,
0x822E9D80,
0x822E9D80,
0x822E9D80,
0x822E9D80,
0x822E9D80,
0x822E9D80,
0x822E9DB4,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
0x822E9D88,
]
# bctr 0x822ea35c in 0x822ea234: absolute table at 0x822ea360, index r3, 110 entries (no bounds check)
[[switch]]
base = 0x822EA350
r = 3
labels = [
0x822EA518,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA520,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA530,
0x822EA528,
0x822EA588,
0x822EA588,
0x822EA550,
0x822EA550,
0x822EA588,
0x822EA538,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA578,
0x822EA580,
0x822EA578,
0x822EA580,
0x822EA580,
0x822EA580,
0x822EA580,
0x822EA560,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA560,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA558,
0x822EA558,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA568,
0x822EA558,
0x822EA558,
0x822EA570,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA570,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA588,
0x822EA548,
0x822EA540,
]
# bctr 0x82301d0c in 0x82301be8: absolute table at 0x82301d10, index r3, 11 entries (no bounds check)
[[switch]]
base = 0x82301D00
r = 3
labels = [
0x82301D8C,
0x82301D3C,
0x82301D44,
0x82301D4C,
0x82301D54,
0x82301D5C,
0x82301D64,
0x82301D6C,
0x82301D74,
0x82301D7C,
0x82301D84,
]
# bctr 0x8249220c in 0x824921f8: absolute table at 0x82492210, index r3, 64 entries (no bounds check)
[[switch]]
base = 0x82492200
r = 3
labels = [
0x82492320,
0x82492320,
0x82492328,
0x82492320,
0x82492328,
0x82492328,
0x82492328,
0x82492310,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492318,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492320,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492328,
0x82492320,
]
# bctr 0x825725c4 in 0x825725a0: absolute table at 0x825725c8, index r3, 7 entries (no bounds check)
[[switch]]
base = 0x825725B8
r = 3
labels = [
0x825725E4,
0x82572600,
0x8257261C,
0x82572638,
0x82572654,
0x82572670,
0x825726C0,
]
# bctr 0x825726fc in 0x825726dc: absolute table at 0x82572700, index r3, 6 entries (no bounds check)
[[switch]]
base = 0x825726F0
r = 3
labels = [
0x82572718,
0x82572728,
0x82572738,
0x82572748,
0x82572758,
0x82572768,
]
# bctr 0x8257278c in 0x825726dc: absolute table at 0x82572790, index r3, 110 entries (no bounds check)
[[switch]]
base = 0x82572780
r = 3
labels = [
0x82572948,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572954,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572960,
0x8257296C,
0x82572A14,
0x82572A14,
0x82572978,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572984,
0x82572A08,
0x825729A8,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x825729B4,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x825729C0,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x825729CC,
0x825729CC,
0x825729CC,
0x825729CC,
0x825729CC,
0x825729CC,
0x82572A14,
0x82572A14,
0x825729D8,
0x825729D8,
0x825729D8,
0x825729D8,
0x825729D8,
0x825729D8,
0x825729FC,
0x82572A14,
0x825729E4,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x825729F0,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x82572A14,
0x8257299C,
0x82572990,
]
# bctr 0x82576fe4 in 0x82576fd0: absolute table at 0x82576fe8, index r3, 8 entries (no bounds check)
[[switch]]
base = 0x82576FD8
r = 3
labels = [
0x82577008,
0x82577010,
0x82577018,
0x82577020,
0x82577028,
0x82577030,
0x82577038,
0x82577040,
]
# bctr 0x82586b34 in 0x82586b1c: absolute table at 0x82586b38, index r3, 6 entries (no bounds check)
[[switch]]
base = 0x82586B28
r = 3
labels = [
0x82586B78,
0x82586B50,
0x82586B58,
0x82586B60,
0x82586B68,
0x82586B70,
]
# bctr 0x825b1ee8 in 0x825b1ed0: absolute table at 0x825b1eec, index r3, 4 entries (no bounds check)
[[switch]]
base = 0x825B1EDC
r = 3
labels = [
0x825B1EFC,
0x825B1F08,
0x825B1F14,
0x825B1F20,
]
# bctr 0x825b1f48 in 0x825b1ed0: absolute table at 0x825b1f4c, index r3, 25 entries (no bounds check)
[[switch]]
base = 0x825B1F3C
r = 3
labels = [
0x825B1FB0,
0x825B1FC8,
0x825B1FD4,
0x825B1FEC,
0x825B1FF8,
0x825B2004,
0x825B2010,
0x825B2004,
0x825B2010,
0x825B201C,
0x825B201C,
0x825B204C,
0x825B204C,
0x825B2028,
0x825B201C,
0x825B204C,
0x825B204C,
0x825B2040,
0x825B1FBC,
0x825B2028,
0x825B1FE0,
0x825B204C,
0x825B2034,
0x825B204C,
0x825B204C,
]
# bctr 0x82780240 in 0x8277fdc0: absolute table at 0x82780244, index r19, 21 entries (no bounds check)
[[switch]]
base = 0x82780234
r = 19
labels = [
0x82780298,
0x82780298,
0x8277FE08,
0x82780298,
0x82780298,
0x82780380,
0x82780298,
0x8278030C,
0x82780298,
0x82780298,
0x8277FE08,
0x8277FE08,
0x82780298,
0x82780298,
0x8277FE08,
0x8277FE08,
0x8277FE08,
0x8277FE08,
0x82780298,
0x82780298,
0x82780298,
]
+71
View File
@@ -0,0 +1,71 @@
# Play testing: what to send when something goes wrong
Thanks for testing. The game keeps its own logs and can pack everything we
need into one file, so a report takes one button press and a few words
from you.
## Installing
- **Steam Deck, Steam Machine, Steam Frame (SteamOS) and Mac:** the downloads
on the Releases page, with the setup steps in the [README](../README.md)
(and the README inside each SteamOS download). The first launch without
game files offers to install them from your own disc image or extracted
folder. In Game Mode, set the shortcut's Properties > General > Game
Resolution to **Native**.
- **From source:** the README's "Building from source", or
[scripts/steamos/README.md](../scripts/steamos/README.md) for SteamOS.
## The menu
- Keyboard: **F1** (on a Mac also **Cmd+,**).
- Controller: **Back + Start** together (on a Steam Deck: the View and Menu
buttons on either side of the screen).
- The D-pad picks a row, left/right changes it, A selects, LB/RB switch
pages, B closes. The game pauses while the menu is open.
## When something goes wrong
1. Open the menu, go to the **Advanced** page (LB/RB, or Q/E) and choose
**Save Bug Report** (A, or Enter).
2. A notice at the bottom of the screen says where the file went:
`speedbreaker-bug-report-v<version>-<date>_<time>.zip` on your **Desktop**
(on SteamOS, `/home/deck/Desktop`: switch to Desktop Mode to pick it up;
on iPhone and iPad, the Files app's SpeedBreaker folder), or, if there is
no Desktop, in the game's data folder (below).
3. Send that .zip with a few words: what you were doing, what you expected,
what happened, and roughly how long into the session (the logs count
seconds from launch).
If the game **crashed**, just launch it again: it says it saved a crash
report, and the next Save Bug Report includes it.
If the game **froze** (no picture change for 20 seconds, outside loading),
it writes a hang report by itself and says so. Open the menu and save a bug
report right there if you can; if you can't, quit (Steam's Exit Game, or
close the window), launch again and save one then: the hang report is kept.
A bug report holds the last five sessions' logs, any crash and hang
reports, your settings.toml, a summary of the machine (OS, CPU, memory, GPU
and driver, display, the settings in effect) and a screenshot of the last
game frame without the menu over it. Paths in your home folder show as `~`.
Nothing is sent anywhere by the game: you send the file.
## Where the logs live
Each launch writes its own log (the newest five are kept), named after the
date and time it started, next to any `crash-*.txt` and `hang-*.txt`
reports:
- Linux / SteamOS: `~/.local/share/speedbreaker/logs/`
(`$XDG_DATA_HOME/speedbreaker/logs/` if that is set)
- macOS: `~/Library/Application Support/SpeedBreaker/logs/`
On SteamOS the launcher also leaves the latest session's output in
`last-run.log`, in the game's folder (`~/Games/SpeedBreaker/` for the
download).
The first lines of every log say which build it is
(`[build] SpeedBreaker v<version> (<commit>), built ...`, also shown at the
bottom of Settings > Advanced), whether this version is new since the last
run (`[version] updated from v<a> to v<b>`, or `first run`), and describe the
machine (`[system] ...`).
@@ -0,0 +1,14 @@
--- a/libavcodec/wmaprodec.c
+++ b/libavcodec/wmaprodec.c
@@ -1923,7 +1923,10 @@
nb_samples = FFMIN(nb_samples, av_audio_fifo_size(s->samples[0][i]));
}
- if (!eof && avpkt->size)
+ /* nfsmw-recomp: only multi-stream files need samples held back to
+ * keep streams in sync; the Xbox 360's XMA decoder (one stream per
+ * context) outputs every decoded frame at once, and games rely on it. */
+ if (!eof && avpkt->size && s->num_streams > 1)
nb_samples -= FFMIN(nb_samples, 4096);
/* copy samples from buffer to output if possible */
@@ -0,0 +1,266 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 04:52:37 +0000
Subject: [PATCH 1/3] Add instructions used by NFS: Most Wanted (Xbox 360)
- Branches on the summary-overflow/unordered CR bit: bso, bns, bsolr, bnslr.
- CR logic: cror, crorc.
- Integer: mulhd, mulhdu; stfsu.
- VMX: vslh, vsrh, vsrah, vrlh, vsrab, vspltish, vmaxsh, vminsh, vavguh,
vsubshs, vaddsbs, vaddsws, vsububm, vandc, vsel128, vpkswss(128),
vpkswus, vpkshss, vpkuhus, vctuxs/vcfpuxws128 (new simde_mm_vctuxs helper),
vcmpgtsh(.), vcmpgtsw(.), vcmpequh(.).
- Record forms for vcmpgtuh./vcmpgtub. now set cr6.
- XenonTests: only pass -march=sandybridge on x86 hosts (arm64 via SIMDe).
Tested with Xenia's instruction tests plus 227 generated cases checked
against an independent Python model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
XenonRecomp/recompiler.cpp | 174 +++++++++++++++++++++++++++++++++++++
XenonTests/CMakeLists.txt | 5 +-
XenonUtils/ppc_context.h | 14 +++
3 files changed, 192 insertions(+), 1 deletion(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index fa91527..01ca7fa 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -1873,10 +1873,14 @@ bool Recompiler::Recompile(
case PPC_INST_VCMPGTUB:
println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_cmpgt_epu8(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.setFromMask(simde_mm_load_si128((simde__m128i*){}.u8), 0xFFFF);", cr(6), v(insn.operands[0]));
break;
case PPC_INST_VCMPGTUH:
println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_cmpgt_epu16(simde_mm_load_si128((simde__m128i*){}.u16), simde_mm_load_si128((simde__m128i*){}.u16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.setFromMask(simde_mm_load_si128((simde__m128i*){}.u8), 0xFFFF);", cr(6), v(insn.operands[0]));
break;
case PPC_INST_VEXPTEFP:
@@ -2087,6 +2091,176 @@ bool Recompiler::Recompile(
println("\tsimde_mm_store_ps({}.f32, simde_mm_div_ps(simde_mm_set1_ps(1), simde_mm_sqrt_ps(simde_mm_load_ps({}.f32))));", v(insn.operands[0]), v(insn.operands[1]));
break;
+ // ---- nfsmw-recomp additions (NFS: Most Wanted 360) ----
+ // Vector registers are stored byte-reversed: PPC element 0 is the host's
+ // highest lane. Element-wise ops are unaffected; packs take (vB, vA).
+
+ case PPC_INST_VSEL128:
+ // vsel128 vD, vA, vB, vD: the mask is the (VS128) destination register.
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_or_si128(simde_mm_andnot_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)), simde_mm_and_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8))));", v(insn.operands[0]), v(insn.operands[3]), v(insn.operands[1]), v(insn.operands[3]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VANDC:
+ // vA & ~vB
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_andnot_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1]));
+ break;
+
+ case PPC_INST_VSLH:
+ for (size_t i = 0; i < 8; i++)
+ println("\t{}.u16[{}] = {}.u16[{}] << ({}.u8[{}] & 0xF);", v(insn.operands[0]), i, v(insn.operands[1]), i, v(insn.operands[2]), i * 2);
+ break;
+
+ case PPC_INST_VSRH:
+ for (size_t i = 0; i < 8; i++)
+ println("\t{}.u16[{}] = {}.u16[{}] >> ({}.u8[{}] & 0xF);", v(insn.operands[0]), i, v(insn.operands[1]), i, v(insn.operands[2]), i * 2);
+ break;
+
+ case PPC_INST_VSRAH:
+ for (size_t i = 0; i < 8; i++)
+ println("\t{}.s16[{}] = {}.s16[{}] >> ({}.u8[{}] & 0xF);", v(insn.operands[0]), i, v(insn.operands[1]), i, v(insn.operands[2]), i * 2);
+ break;
+
+ case PPC_INST_VRLH:
+ for (size_t i = 0; i < 8; i++)
+ println("\t{}.u16[{}] = uint16_t(({}.u16[{}] << ({}.u8[{}] & 0xF)) | ({}.u16[{}] >> ((16 - ({}.u8[{}] & 0xF)) & 0xF)));",
+ v(insn.operands[0]), i, v(insn.operands[1]), i, v(insn.operands[2]), i * 2, v(insn.operands[1]), i, v(insn.operands[2]), i * 2);
+ break;
+
+ case PPC_INST_VSRAB:
+ for (size_t i = 0; i < 16; i++)
+ println("\t{}.s8[{}] = {}.s8[{}] >> ({}.u8[{}] & 0x7);", v(insn.operands[0]), i, v(insn.operands[1]), i, v(insn.operands[2]), i);
+ break;
+
+ case PPC_INST_VSPLTISH:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u16, simde_mm_set1_epi16(int16_t(0x{:X})));", v(insn.operands[0]), insn.operands[1] & 0xFFFF);
+ break;
+
+ case PPC_INST_VMAXSH:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.s16, simde_mm_max_epi16(simde_mm_load_si128((simde__m128i*){}.s16), simde_mm_load_si128((simde__m128i*){}.s16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VMINSH:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.s16, simde_mm_min_epi16(simde_mm_load_si128((simde__m128i*){}.s16), simde_mm_load_si128((simde__m128i*){}.s16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VAVGUH:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u16, simde_mm_avg_epu16(simde_mm_load_si128((simde__m128i*){}.u16), simde_mm_load_si128((simde__m128i*){}.u16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VSUBSHS:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.s16, simde_mm_subs_epi16(simde_mm_load_si128((simde__m128i*){}.s16), simde_mm_load_si128((simde__m128i*){}.s16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VADDSBS:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.s8, simde_mm_adds_epi8(simde_mm_load_si128((simde__m128i*){}.s8), simde_mm_load_si128((simde__m128i*){}.s8)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VSUBUBM:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_sub_epi8(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ break;
+
+ case PPC_INST_VADDSWS:
+ for (size_t i = 0; i < 4; i++)
+ {
+ println("\t{}.s64 = int64_t({}.s32[{}]) + int64_t({}.s32[{}]);", temp(), v(insn.operands[1]), i, v(insn.operands[2]), i);
+ println("\t{}.s32[{}] = {}.s64 > INT_MAX ? INT_MAX : {}.s64 < INT_MIN ? INT_MIN : {}.s64;", v(insn.operands[0]), i, temp(), temp(), temp());
+ }
+ break;
+
+ case PPC_INST_VPKSWSS:
+ case PPC_INST_VPKSWSS128:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.s16, simde_mm_packs_epi32(simde_mm_load_si128((simde__m128i*){}.s32), simde_mm_load_si128((simde__m128i*){}.s32)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1]));
+ break;
+
+ case PPC_INST_VPKSWUS:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u16, simde_mm_packus_epi32(simde_mm_load_si128((simde__m128i*){}.s32), simde_mm_load_si128((simde__m128i*){}.s32)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1]));
+ break;
+
+ case PPC_INST_VPKSHSS:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.s8, simde_mm_packs_epi16(simde_mm_load_si128((simde__m128i*){}.s16), simde_mm_load_si128((simde__m128i*){}.s16)));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1]));
+ break;
+
+ case PPC_INST_VPKUHUS:
+ // Unsigned in, unsigned out: clamp to 0xFF first, since packus treats input as signed.
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_packus_epi16(simde_mm_min_epu16(simde_mm_load_si128((simde__m128i*){}.u16), simde_mm_set1_epi16(0xFF)), simde_mm_min_epu16(simde_mm_load_si128((simde__m128i*){}.u16), simde_mm_set1_epi16(0xFF))));", v(insn.operands[0]), v(insn.operands[2]), v(insn.operands[1]));
+ break;
+
+ case PPC_INST_VCTUXS:
+ case PPC_INST_VCFPUXWS128:
+ printSetFlushMode(true);
+ print("\tsimde_mm_store_si128((simde__m128i*){}.u32, simde_mm_vctuxs(", v(insn.operands[0]));
+ if (insn.operands[2] != 0)
+ println("simde_mm_mul_ps(simde_mm_load_ps({}.f32), simde_mm_set1_ps({}))));", v(insn.operands[1]), 1u << insn.operands[2]);
+ else
+ println("simde_mm_load_ps({}.f32)));", v(insn.operands[1]));
+ break;
+
+ case PPC_INST_VCMPGTSH:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_cmpgt_epi16(simde_mm_load_si128((simde__m128i*){}.s16), simde_mm_load_si128((simde__m128i*){}.s16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.setFromMask(simde_mm_load_si128((simde__m128i*){}.u8), 0xFFFF);", cr(6), v(insn.operands[0]));
+ break;
+
+ case PPC_INST_VCMPGTSW:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_cmpgt_epi32(simde_mm_load_si128((simde__m128i*){}.s32), simde_mm_load_si128((simde__m128i*){}.s32)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.setFromMask(simde_mm_load_si128((simde__m128i*){}.u8), 0xFFFF);", cr(6), v(insn.operands[0]));
+ break;
+
+ case PPC_INST_VCMPEQUH:
+ println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_cmpeq_epi16(simde_mm_load_si128((simde__m128i*){}.u16), simde_mm_load_si128((simde__m128i*){}.u16)));", v(insn.operands[0]), v(insn.operands[1]), v(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.setFromMask(simde_mm_load_si128((simde__m128i*){}.u8), 0xFFFF);", cr(6), v(insn.operands[0]));
+ break;
+
+ case PPC_INST_BSO:
+ printConditionalBranch(false, "so");
+ break;
+
+ case PPC_INST_BNS:
+ printConditionalBranch(true, "so");
+ break;
+
+ case PPC_INST_BSOLR:
+ println("\tif ({}.so) return;", cr(insn.operands[0]));
+ break;
+
+ case PPC_INST_BNSLR:
+ println("\tif (!{}.so) return;", cr(insn.operands[0]));
+ break;
+
+ case PPC_INST_CROR:
+ case PPC_INST_CRORC:
+ {
+ // Operands are CR bit numbers: field = bit / 4; lt, gt, eq, so.
+ static constexpr const char* CR_BITS[] = { "lt", "gt", "eq", "so" };
+ auto bit = [&](size_t b) { return fmt::format("{}.{}", cr(b / 4), CR_BITS[b % 4]); };
+ println("\t{} = {} | {}{};", bit(insn.operands[0]), bit(insn.operands[1]),
+ insn.opcode->id == PPC_INST_CRORC ? "!" : "", bit(insn.operands[2]));
+ break;
+ }
+
+ case PPC_INST_MULHD:
+ println("\t{}.s64 = int64_t((__int128_t({}.s64) * __int128_t({}.s64)) >> 64);", r(insn.operands[0]), r(insn.operands[1]), r(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.compare<int64_t>({}.s64, 0, {});", cr(0), r(insn.operands[0]), xer());
+ break;
+
+ case PPC_INST_MULHDU:
+ println("\t{}.u64 = uint64_t((__uint128_t({}.u64) * __uint128_t({}.u64)) >> 64);", r(insn.operands[0]), r(insn.operands[1]), r(insn.operands[2]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t{}.compare<int64_t>({}.s64, 0, {});", cr(0), r(insn.operands[0]), xer());
+ break;
+
+ case PPC_INST_STFSU:
+ printSetFlushMode(false);
+ println("\t{}.f32 = float({}.f64);", temp(), f(insn.operands[0]));
+ println("\t{} = {} + {}.u32;", ea(), int32_t(insn.operands[1]), r(insn.operands[2]));
+ println("\tPPC_STORE_U32({}, {}.u32);", ea(), temp());
+ println("\t{}.u32 = {};", r(insn.operands[2]), ea());
+ break;
+ // ---- end nfsmw-recomp additions ----
+
case PPC_INST_VSEL:
println("\tsimde_mm_store_si128((simde__m128i*){}.u8, simde_mm_or_si128(simde_mm_andnot_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8)), simde_mm_and_si128(simde_mm_load_si128((simde__m128i*){}.u8), simde_mm_load_si128((simde__m128i*){}.u8))));", v(insn.operands[0]), v(insn.operands[3]), v(insn.operands[1]), v(insn.operands[3]), v(insn.operands[2]));
break;
diff --git a/XenonTests/CMakeLists.txt b/XenonTests/CMakeLists.txt
index 8beb23c..5eb8670 100644
--- a/XenonTests/CMakeLists.txt
+++ b/XenonTests/CMakeLists.txt
@@ -13,8 +13,11 @@ if(TEST_FILES)
)
target_compile_options(XenonTests
PRIVATE
- "-march=sandybridge"
"-Wno-unused-label"
"-Wno-unused-variable"
)
+ # x86 hosts get native SSE/AVX; everything else (arm64) goes through SIMDe.
+ if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
+ target_compile_options(XenonTests PRIVATE "-march=sandybridge")
+ endif()
endif()
diff --git a/XenonUtils/ppc_context.h b/XenonUtils/ppc_context.h
index 31a86cd..745e1c0 100644
--- a/XenonUtils/ppc_context.h
+++ b/XenonUtils/ppc_context.h
@@ -682,6 +682,20 @@ inline simde__m128i simde_mm_vctsxs(simde__m128 src1)
return simde_mm_andnot_si128(simde_mm_castps_si128(xmm2), simde_mm_castps_si128(dest));
}
+// vctuxs: float -> uint32 with saturation. NaN and negatives give 0, values
+// >= 2^32 give 0xFFFFFFFF, everything else truncates toward zero.
+inline simde__m128i simde_mm_vctuxs(simde__m128 src)
+{
+ simde__m128 ordered = simde_mm_cmpord_ps(src, src);
+ simde__m128 clamped = simde_mm_and_ps(simde_mm_max_ps(src, simde_mm_setzero_ps()), ordered);
+ simde__m128 two31 = simde_mm_set1_ps(2147483648.0f);
+ simde__m128i big = simde_mm_castps_si128(simde_mm_cmpge_ps(clamped, two31));
+ simde__m128 adjusted = simde_mm_sub_ps(clamped, simde_mm_and_ps(simde_mm_castsi128_ps(big), two31));
+ simde__m128i result = simde_mm_xor_si128(simde_mm_cvttps_epi32(adjusted), simde_mm_and_si128(big, simde_mm_set1_epi32(INT_MIN)));
+ simde__m128i over = simde_mm_castps_si128(simde_mm_cmpge_ps(clamped, simde_mm_set1_ps(4294967296.0f)));
+ return simde_mm_or_si128(result, over);
+}
+
inline simde__m128i simde_mm_vsr(simde__m128i a, simde__m128i b)
{
b = simde_mm_srli_epi64(simde_mm_slli_epi64(b, 61), 61);
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,32 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 04:53:53 +0000
Subject: [PATCH 2/3] arm64: widen the FPCR write operand to 64 bits
`msr fpcr, %0` with a uint32_t operand leaves the upper half of the
register unspecified (clang: -Wasm-operand-widths), and it lands in FPCR's
reserved bits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
XenonUtils/ppc_context.h | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/XenonUtils/ppc_context.h b/XenonUtils/ppc_context.h
index 745e1c0..fb2c613 100644
--- a/XenonUtils/ppc_context.h
+++ b/XenonUtils/ppc_context.h
@@ -251,7 +251,9 @@ struct PPCFPSCRRegister
inline void setcsr(uint32_t csr) noexcept
{
- __asm__ __volatile__("msr fpcr, %0" : : "r"(csr));
+ // msr needs a 64-bit register; a uint32_t operand leaves the upper
+ // half unspecified and it would land in FPCR's reserved bits.
+ __asm__ __volatile__("msr fpcr, %0" : : "r"(uint64_t(csr)));
}
#else
# error "Missing implementation for FPSCR."
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,142 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 15:05:03 +0000
Subject: [PATCH 3/3] Secondary entry points: functions entered inside another
function's body
MSVC can share one function body between several entry points; a title may
call the inner one only through a pointer. New config key
secondary_entries = [addr, ...] emits sub_<addr> with the enclosing
function's body entered by a goto (placed after the local declarations),
and registers it for direct and indirect calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
XenonRecomp/recompiler.cpp | 27 +++++++++++++++++++++++++--
XenonRecomp/recompiler.h | 3 ++-
XenonRecomp/recompiler_config.cpp | 6 ++++++
XenonRecomp/recompiler_config.h | 4 ++++
4 files changed, 37 insertions(+), 3 deletions(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index 01ca7fa..5d822b1 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -2456,7 +2456,7 @@ bool Recompiler::Recompile(
return true;
}
-bool Recompiler::Recompile(const Function& fn)
+bool Recompiler::Recompile(const Function& fn, uint32_t entry)
{
auto base = fn.base;
auto end = base + fn.size;
@@ -2464,6 +2464,8 @@ bool Recompiler::Recompile(const Function& fn)
static std::unordered_set<size_t> labels;
labels.clear();
+ if (entry != 0)
+ labels.emplace(entry);
for (size_t addr = base; addr < end; addr += 4)
{
@@ -2546,7 +2548,11 @@ bool Recompiler::Recompile(const Function& fn)
auto symbol = image.symbols.find(fn.base);
std::string name;
- if (symbol != image.symbols.end())
+ if (entry != 0)
+ {
+ name = fmt::format("sub_{:X}", entry);
+ }
+ else if (symbol != image.symbols.end())
{
name = symbol->name;
}
@@ -2669,6 +2675,10 @@ bool Recompiler::Recompile(const Function& fn)
if (localVariables.ea)
println("\tuint32_t ea{{}};");
+ // Secondary entry: jump in after the local declarations (C++ forbids
+ // jumping past their initialisation).
+ if (entry != 0)
+ println("\tgoto loc_{:X};", entry);
out += tempString;
return allRecompiled;
@@ -2752,6 +2762,9 @@ void Recompiler::Recompile(const std::filesystem::path& headerFilePath)
SaveCurrentOutData("ppc_context.h");
}
+ for (uint32_t entry : config.secondaryEntries)
+ image.symbols.emplace(fmt::format("sub_{:X}", entry), entry, 4, Symbol_Function);
+
{
println("#pragma once\n");
println("#include \"ppc_config.h\"");
@@ -2790,6 +2803,16 @@ void Recompiler::Recompile(const std::filesystem::path& headerFilePath)
Recompile(functions[i]);
}
+ for (uint32_t entry : config.secondaryEntries)
+ {
+ auto fn = std::find_if(functions.begin(), functions.end(),
+ [&](const Function& f) { return entry > f.base && entry < f.base + f.size; });
+ if (fn == functions.end())
+ fmt::println("ERROR: secondary entry {:X} is not inside any function", entry);
+ else
+ Recompile(*fn, entry);
+ }
+
SaveCurrentOutData();
}
diff --git a/XenonRecomp/recompiler.h b/XenonRecomp/recompiler.h
index 8242db5..690570b 100644
--- a/XenonRecomp/recompiler.h
+++ b/XenonRecomp/recompiler.h
@@ -62,7 +62,8 @@ struct Recompiler
RecompilerLocalVariables& localVariables,
CSRState& csrState);
- bool Recompile(const Function& fn);
+ // `entry` != 0: emit the function named after `entry`, entered at it (secondary entry point).
+ bool Recompile(const Function& fn, uint32_t entry = 0);
void Recompile(const std::filesystem::path& headerFilePath);
diff --git a/XenonRecomp/recompiler_config.cpp b/XenonRecomp/recompiler_config.cpp
index 81330a4..1623367 100644
--- a/XenonRecomp/recompiler_config.cpp
+++ b/XenonRecomp/recompiler_config.cpp
@@ -58,6 +58,12 @@ void RecompilerConfig::Load(const std::string_view& configFilePath)
}
}
+ if (auto entriesArray = main["secondary_entries"].as_array())
+ {
+ for (auto& entry : *entriesArray)
+ secondaryEntries.push_back(*entry.value<uint32_t>());
+ }
+
if (auto invalidArray = main["invalid_instructions"].as_array())
{
for (auto& instr : *invalidArray)
diff --git a/XenonRecomp/recompiler_config.h b/XenonRecomp/recompiler_config.h
index 534e503..581e796 100644
--- a/XenonRecomp/recompiler_config.h
+++ b/XenonRecomp/recompiler_config.h
@@ -50,6 +50,10 @@ struct RecompilerConfig
uint32_t longJmpAddress = 0;
uint32_t setJmpAddress = 0;
std::unordered_map<uint32_t, uint32_t> functions;
+ // Addresses that are entered from outside but lie inside another function
+ // (MSVC shares one body between several entry points). Each gets its own
+ // function: the enclosing function's body, entered with a goto.
+ std::vector<uint32_t> secondaryEntries;
std::unordered_map<uint32_t, uint32_t> invalidInstructions;
std::unordered_map<uint32_t, RecompilerMidAsmHook> midAsmHooks;
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,32 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:11:22 +0000
Subject: [PATCH] Switch tables: index with the low 32 bits
A bounds-checked jump table compares the index with cmplwi and loads the
entry with rlwinm/lwzx, both 32-bit. 64-bit arithmetic before them (e.g.
addis) can leave carries in the upper half, so switching on the full
64-bit register can miss every case and hit __builtin_unreachable.
Seen in NFS: Most Wanted's sub_8217F390 (addis r11,r11,32764).
---
XenonRecomp/recompiler.cpp | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index 5d822b1..9835368 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -609,7 +609,9 @@ bool Recompiler::Recompile(
case PPC_INST_BCTR:
if (switchTable != config.switchTables.end())
{
- println("\tswitch ({}.u64) {{", r(switchTable->second.r));
+ // The table index is the low word: 64-bit adds on the way (addis)
+ // can leave carries in the upper half that the hardware ignores.
+ println("\tswitch ({}.u32) {{", r(switchTable->second.r));
for (size_t i = 0; i < switchTable->second.labels.size(); i++)
{
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:18:20 +0000
Subject: [PATCH] Memory barriers: emit fences for sync, lwsync and eieio
They were no-ops, which only works on hosts with strong ordering (x86
TSO). On ARM64 the hardware and the compiler may reorder around them;
NFS: Most Wanted's command queues then read a command slot before the
producer's writes are visible and call a null function pointer.
---
XenonRecomp/recompiler.cpp | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index 9835368..ff3103e 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -851,7 +851,8 @@ bool Recompiler::Recompile(
break;
case PPC_INST_EIEIO:
- // no op
+ // Orders stores; a no-op on x86 (TSO) but not on ARM64.
+ println("\t__atomic_thread_fence(__ATOMIC_RELEASE);");
break;
case PPC_INST_EXTSB:
@@ -1211,7 +1212,8 @@ bool Recompiler::Recompile(
break;
case PPC_INST_LWSYNC:
- // no op
+ // Weakly ordered hosts (ARM64) need the barrier the guest asked for.
+ println("\t__atomic_thread_fence(__ATOMIC_ACQ_REL);");
break;
case PPC_INST_LWZ:
@@ -1713,7 +1715,8 @@ bool Recompiler::Recompile(
break;
case PPC_INST_SYNC:
- // no op
+ // Weakly ordered hosts (ARM64) need the barrier the guest asked for.
+ println("\t__atomic_thread_fence(__ATOMIC_SEQ_CST);");
break;
case PPC_INST_TDLGEI:
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,202 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:28:03 +0000
Subject: [PATCH] vpkd3d128: implement every pack type and merge (Xenia
semantics)
Only D3DCOLOR and FLOAT16_4 with fixed merges were handled; other types
emitted a debug trap. NFS: Most Wanted uses NORMSHORT4 (type 4, pack 2)
in-game. A helper in ppc_context.h packs magic-biased floats as Xenia's
PACK sequences do (clamp max-then-min, low mantissa bits; float16 rounds
toward zero) and merges with Xenia's permute controls for every pack and
shift.
---
XenonRecomp/recompiler.cpp | 48 +---------------
XenonUtils/ppc_context.h | 112 +++++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+), 45 deletions(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index ff3103e..4e066dd 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -2004,51 +2004,9 @@ bool Recompiler::Recompile(
}
case PPC_INST_VPKD3D128:
- // TODO: vectorize somehow?
- // NOTE: handling vector reversal here too
- printSetFlushMode(true);
- switch (insn.operands[2])
- {
- case 0: // D3D color
- if (insn.operands[3] != 1)
- fmt::println("Unexpected D3D color pack instruction at {:X}", base);
-
- for (size_t i = 0; i < 4; i++)
- {
- constexpr size_t indices[] = { 3, 0, 1, 2 };
- println("\t{}.u32[{}] = 0x404000FF;", vTemp(), i);
- println("\t{}.f32[{}] = {}.f32[{}] < 3.0f ? 3.0f : ({}.f32[{}] > {}.f32[{}] ? {}.f32[{}] : {}.f32[{}]);", vTemp(), i, v(insn.operands[1]), i, v(insn.operands[1]), i, vTemp(), i, vTemp(), i, v(insn.operands[1]), i);
- println("\t{}.u32 {}= uint32_t({}.u8[{}]) << {};", temp(), i == 0 ? "" : "|", vTemp(), i * 4, indices[i] * 8);
- }
- println("\t{}.u32[{}] = {}.u32;", v(insn.operands[0]), insn.operands[4], temp());
- break;
-
- case 5: // float16_4
- if (insn.operands[3] != 2 || insn.operands[4] > 2)
- fmt::println("Unexpected float16_4 pack instruction at {:X}", base);
-
- for (size_t i = 0; i < 4; i++)
- {
- // Strip sign from source
- println("\t{}.u32 = ({}.u32[{}]&0x7FFFFFFF);", temp(), v(insn.operands[1]), i);
- // If |source| is > 65504, clamp output to 0x7FFF, else save 8 exponent bits
- println("\t{0}.u8[0] = ({1}.f32 != {1}.f32) || ({1}.f32 > 65504.0f) ? 0xFF : (({2}.u32[{3}]&0x7f800000)>>23);", vTemp(), temp(), v(insn.operands[1]), i);
- // If 8 exponent bits were saved, it can only be 0x8E at most
- // If saved, save first 10 bits of mantissa
- println("\t{}.u16 = {}.u8[0] != 0xFF ? (({}.u32[{}]&0x7FE000)>>13) : 0x0;", temp(), vTemp(), v(insn.operands[1]), i);
- // If saved and > 127-15, exponent is converted from 8 to 5-bit by subtracting 0x70
- // If saved but not > 127-15, clamp exponent at 0, add 0x400 to mantissa and shift right by (0x71-exponent)
- // If right shift is greater than 31 bits, manually clamp mantissa to 0 or else the output of the shift will be wrong
- println("\t{0}.u16[{1}] = {2}.u8[0] != 0xFF ? ({2}.u8[0] > 0x70 ? ((({2}.u8[0]-0x70)<<10)+{3}.u16) : (0x71-{2}.u8[0] > 31 ? 0x0 : ((0x400+{3}.u16)>>(0x71-{2}.u8[0])))) : 0x7FFF;", v(insn.operands[0]), i+(2*insn.operands[4]), vTemp(), temp());
- // Add back original sign
- println("\t{}.u16[{}] |= (({}.u32[{}]&0x80000000)>>16);", v(insn.operands[0]), i+(2*insn.operands[4]), v(insn.operands[1]), i);
- }
- break;
-
- default:
- println("\t__builtin_debugtrap();");
- break;
- }
+ // vD, vB, type, pack, shift: see ppc_vpkd3d128 in ppc_context.h.
+ println("\tppc_vpkd3d128({}, {}, {}, {}, {});", v(insn.operands[0]), v(insn.operands[1]),
+ insn.operands[2], insn.operands[3], insn.operands[4]);
break;
case PPC_INST_VPKSHUS:
diff --git a/XenonUtils/ppc_context.h b/XenonUtils/ppc_context.h
index fb2c613..865f3f4 100644
--- a/XenonUtils/ppc_context.h
+++ b/XenonUtils/ppc_context.h
@@ -205,6 +205,118 @@ union alignas(0x10) PPCVRegister
double f64[2];
};
+// vpkd3d128 (VMX128): pack a vector of "magic"-biased floats into a D3D
+// vertex format and merge the result into vD. Semantics from Xenia
+// (ppc_emit_altivec.cc InstrEmit_vpkd3d128 and the x64 PACK sequences).
+// PPC element k (x = 0) is u32[3 - k] here: registers are byte-reversed.
+inline uint16_t ppc_float_to_half_rtz(float value)
+{
+ uint32_t f;
+ memcpy(&f, &value, 4);
+ uint32_t sign = (f >> 16) & 0x8000, exponent = (f >> 23) & 0xFF, mantissa = f & 0x7FFFFF;
+ if (exponent == 0xFF)
+ return uint16_t(sign | 0x7C00 | (mantissa ? 0x200 | (mantissa >> 13) : 0));
+ int e = int(exponent) - 127 + 15;
+ if (e >= 31)
+ return uint16_t(sign | 0x7BFF); // toward zero: the largest finite value
+ if (e <= 0)
+ {
+ if (e < -10)
+ return uint16_t(sign);
+ return uint16_t(sign | ((mantissa | 0x800000) >> (14 - e)));
+ }
+ return uint16_t(sign | (uint32_t(e) << 10) | (mantissa >> 13));
+}
+
+inline uint32_t ppc_pack_clamp_bits(float value, uint32_t lo, uint32_t hi)
+{
+ float flo, fhi;
+ memcpy(&flo, &lo, 4);
+ memcpy(&fhi, &hi, 4);
+ float r = value > flo ? value : flo; // max first: NaN packs as the minimum
+ r = r < fhi ? r : fhi;
+ uint32_t u;
+ memcpy(&u, &r, 4);
+ return u;
+}
+
+inline void ppc_vpkd3d128(PPCVRegister& d, const PPCVRegister& b, uint32_t type, uint32_t pack, uint32_t shift)
+{
+ float e[4] = { b.f32[3], b.f32[2], b.f32[1], b.f32[0] };
+ uint32_t p[4] = {};
+ switch (type)
+ {
+ case 0: // D3DCOLOR: ARGB from w, x, y, z
+ {
+ uint32_t c[4];
+ for (int k = 0; k < 4; k++)
+ c[k] = ppc_pack_clamp_bits(e[k], 0x40400000u, 0x404000FFu) & 0xFF;
+ p[3] = (c[3] << 24) | (c[0] << 16) | (c[1] << 8) | c[2];
+ break;
+ }
+ case 1: // NORMSHORT2
+ case 4: // NORMSHORT4
+ {
+ uint32_t s[4];
+ for (int k = 0; k < 4; k++)
+ s[k] = ppc_pack_clamp_bits(e[k], 0x403F8001u, 0x40407FFFu) & 0xFFFF;
+ if (type == 1)
+ p[3] = (s[0] << 16) | s[1];
+ else
+ {
+ p[2] = (s[0] << 16) | s[1];
+ p[3] = (s[2] << 16) | s[3];
+ }
+ break;
+ }
+ case 2: // NORMPACKED32 2_10_10_10, in every word
+ {
+ uint32_t v = (ppc_pack_clamp_bits(e[0], 0x403FFE01u, 0x404001FFu) & 0x3FF) |
+ ((ppc_pack_clamp_bits(e[1], 0x403FFE01u, 0x404001FFu) & 0x3FF) << 10) |
+ ((ppc_pack_clamp_bits(e[2], 0x403FFE01u, 0x404001FFu) & 0x3FF) << 20) |
+ ((ppc_pack_clamp_bits(e[3], 0x40400000u, 0x40400003u) & 0x3) << 30);
+ p[0] = p[1] = p[2] = p[3] = v;
+ break;
+ }
+ case 3: // FLOAT16_2
+ p[3] = (uint32_t(ppc_float_to_half_rtz(e[0])) << 16) | ppc_float_to_half_rtz(e[1]);
+ break;
+ case 5: // FLOAT16_4
+ p[2] = (uint32_t(ppc_float_to_half_rtz(e[0])) << 16) | ppc_float_to_half_rtz(e[1]);
+ p[3] = (uint32_t(ppc_float_to_half_rtz(e[2])) << 16) | ppc_float_to_half_rtz(e[3]);
+ break;
+ default: // NORMPACKED64 4_20_20_20: not seen yet
+ __builtin_debugtrap();
+ return;
+ }
+
+ // Merge (Xenia's permute controls): {from packed?, word} per element.
+ static const uint8_t merges[4][4][4][2] = {
+ {},
+ { { { 0, 0 }, { 0, 1 }, { 0, 2 }, { 1, 3 } }, { { 0, 0 }, { 0, 1 }, { 1, 3 }, { 0, 3 } },
+ { { 0, 0 }, { 1, 3 }, { 0, 2 }, { 0, 3 } }, { { 1, 3 }, { 0, 1 }, { 0, 2 }, { 0, 3 } } },
+ { { { 0, 0 }, { 0, 1 }, { 1, 2 }, { 1, 3 } }, { { 0, 0 }, { 1, 2 }, { 1, 3 }, { 0, 3 } },
+ { { 1, 2 }, { 1, 3 }, { 0, 2 }, { 0, 3 } }, { { 1, 3 }, { 0, 1 }, { 0, 2 }, { 0, 3 } } },
+ { { { 0, 0 }, { 0, 1 }, { 1, 2 }, { 1, 3 } }, { { 0, 0 }, { 1, 2 }, { 1, 3 }, { 0, 3 } },
+ { { 1, 2 }, { 1, 3 }, { 0, 2 }, { 0, 3 } }, { { 0, 0 }, { 0, 1 }, { 0, 2 }, { 1, 2 } } },
+ };
+ if (pack == 0 || pack > 3 || shift > 3)
+ {
+ __builtin_debugtrap();
+ return;
+ }
+ uint32_t old[4] = { d.u32[3], d.u32[2], d.u32[1], d.u32[0] };
+ uint32_t result[4];
+ for (int i = 0; i < 4; i++)
+ {
+ const uint8_t* m = merges[pack][shift][i];
+ result[i] = m[0] ? p[m[1]] : old[m[1]];
+ }
+ for (int i = 0; i < 4; i++)
+ d.u32[3 - i] = result[i];
+}
+
+
#define PPC_ROUND_NEAREST 0x00
#define PPC_ROUND_TOWARD_ZERO 0x01
#define PPC_ROUND_UP 0x02
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,172 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Fri, 25 Sep 2026 19:31:02 +0000
Subject: [PATCH] vcmpbfp(128) and vupkd3d128: implement (Xenia semantics)
vcmpbfp emitted a debug trap; vupkd3d128 handled only D3DCOLOR and
NORMSHORT2. Bounds compare sets bit 31 for a > b and bit 30 for
!(a >= -b), with CR6 from the out-of-bounds mask for the dotted form.
Unpacking produces Xenia's magic-biased floats for every type except
NORMPACKED64 (unused so far).
---
XenonRecomp/recompiler.cpp | 43 ++++++--------------
XenonUtils/ppc_context.h | 82 ++++++++++++++++++++++++++++++++++++++
2 files changed, 95 insertions(+), 30 deletions(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index 4e066dd..147f55d 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -1836,7 +1836,17 @@ bool Recompiler::Recompile(
case PPC_INST_VCMPBFP:
case PPC_INST_VCMPBFP128:
- println("\t__builtin_debugtrap();");
+ // Bounds compare (Xenia): bit 31 if a > b, bit 30 if !(a >= -b).
+ printSetFlushMode(true);
+ println("\t{{");
+ println("\t\tsimde__m128 a = simde_mm_load_ps({}.f32), b = simde_mm_load_ps({}.f32);", v(insn.operands[1]), v(insn.operands[2]));
+ println("\t\tsimde__m128 gt = simde_mm_cmpgt_ps(a, b);");
+ println("\t\tsimde__m128 lt = simde_mm_cmpnge_ps(a, simde_mm_xor_ps(b, simde_mm_castsi128_ps(simde_mm_set1_epi32(int(0x80000000)))));");
+ println("\t\tsimde_mm_store_ps({}.f32, simde_mm_or_ps(simde_mm_and_ps(gt, simde_mm_castsi128_ps(simde_mm_set1_epi32(int(0x80000000)))), "
+ "simde_mm_and_ps(lt, simde_mm_castsi128_ps(simde_mm_set1_epi32(0x40000000)))));", v(insn.operands[0]));
+ if (strchr(insn.opcode->name, '.'))
+ println("\t\t{}.setFromMask(simde_mm_or_ps(gt, lt), 0xF);", cr(6));
+ println("\t}}");
break;
case PPC_INST_VCMPEQFP:
@@ -2324,35 +2334,8 @@ bool Recompiler::Recompile(
break;
case PPC_INST_VUPKD3D128:
- // TODO: vectorize somehow?
- // NOTE: handling vector reversal here too
- switch (insn.operands[2] >> 2)
- {
- case 0: // D3D color
- for (size_t i = 0; i < 4; i++)
- {
- constexpr size_t indices[] = { 3, 0, 1, 2 };
- println("\t{}.u32[{}] = {}.u8[{}] | 0x3F800000;", vTemp(), i, v(insn.operands[1]), indices[i]);
- }
- println("\t{} = {};", v(insn.operands[0]), vTemp());
- break;
-
- case 1: // 2 shorts
- for (size_t i = 0; i < 2; i++)
- {
- println("\t{}.f32 = 3.0f;", temp());
- println("\t{}.s32 += {}.s16[{}];", temp(), v(insn.operands[1]), 1 - i);
- println("\t{}.f32[{}] = {}.f32;", vTemp(), 3 - i, temp());
- }
- println("\t{}.f32[1] = 0.0f;", vTemp());
- println("\t{}.f32[0] = 1.0f;", vTemp());
- println("\t{} = {};", v(insn.operands[0]), vTemp());
- break;
-
- default:
- println("\t__builtin_debugtrap();");
- break;
- }
+ // vD, vB, IMM (type = IMM >> 2): see ppc_vupkd3d128 in ppc_context.h.
+ println("\tppc_vupkd3d128({}, {}, {});", v(insn.operands[0]), v(insn.operands[1]), insn.operands[2] >> 2);
break;
case PPC_INST_VUPKHSB:
diff --git a/XenonUtils/ppc_context.h b/XenonUtils/ppc_context.h
index 865f3f4..a80eff4 100644
--- a/XenonUtils/ppc_context.h
+++ b/XenonUtils/ppc_context.h
@@ -240,6 +240,88 @@ inline uint32_t ppc_pack_clamp_bits(float value, uint32_t lo, uint32_t hi)
return u;
}
+inline uint32_t ppc_half_to_float_bits(uint32_t h)
+{
+ uint32_t sign = (h & 0x8000) << 16, exponent = (h >> 10) & 0x1F, mantissa = h & 0x3FF;
+ if (exponent == 0x1F)
+ return sign | 0x7F800000 | (mantissa << 13);
+ if (exponent == 0)
+ {
+ if (mantissa == 0)
+ return sign;
+ int e = -1;
+ do
+ {
+ e++;
+ mantissa <<= 1;
+ } while ((mantissa & 0x400) == 0);
+ return sign | (uint32_t(127 - 15 - e) << 23) | ((mantissa & 0x3FF) << 13);
+ }
+ return sign | ((exponent + 127 - 15) << 23) | (mantissa << 13);
+}
+
+// vupkd3d128 (VMX128): unpack a D3D vertex format into "magic"-biased
+// floats (3.0 + n * 2^-22 for signed normalised values, 1.0 + c * 2^-23 for
+// colours; negative overflow becomes a quiet NaN). Semantics from Xenia's
+// x64 UNPACK sequences.
+inline void ppc_vupkd3d128(PPCVRegister& d, const PPCVRegister& b, uint32_t type)
+{
+ uint32_t w[4] = { b.u32[3], b.u32[2], b.u32[1], b.u32[0] }; // PPC order
+ auto short3 = [](uint32_t v) {
+ uint32_t r = 0x40400000u + uint32_t(int32_t(int16_t(v & 0xFFFF)));
+ return r == 0x403F8000u ? 0x7FC00000u : r;
+ };
+ auto tenbit3 = [](uint32_t v) {
+ uint32_t r = 0x40400000u + uint32_t(int32_t((v & 0x3FF) << 22) >> 22);
+ return r == 0x403FFE00u ? 0x7FC00000u : r;
+ };
+ uint32_t r[4];
+ switch (type)
+ {
+ case 0: // D3DCOLOR (ARGB in w) -> RGBA
+ r[0] = 0x3F800000u | ((w[3] >> 16) & 0xFF);
+ r[1] = 0x3F800000u | ((w[3] >> 8) & 0xFF);
+ r[2] = 0x3F800000u | (w[3] & 0xFF);
+ r[3] = 0x3F800000u | (w[3] >> 24);
+ break;
+ case 1: // NORMSHORT2 (in w)
+ r[0] = short3(w[3] >> 16);
+ r[1] = short3(w[3]);
+ r[2] = 0;
+ r[3] = 0x3F800000u;
+ break;
+ case 2: // NORMPACKED32 2_10_10_10 (in w)
+ r[0] = tenbit3(w[3]);
+ r[1] = tenbit3(w[3] >> 10);
+ r[2] = tenbit3(w[3] >> 20);
+ r[3] = 0x3F800000u + ((w[3] >> 30) & 3);
+ break;
+ case 3: // FLOAT16_2 (in w)
+ r[0] = ppc_half_to_float_bits(w[3] >> 16);
+ r[1] = ppc_half_to_float_bits(w[3] & 0xFFFF);
+ r[2] = 0;
+ r[3] = 0x3F800000u;
+ break;
+ case 4: // NORMSHORT4 (in z, w)
+ r[0] = short3(w[2] >> 16);
+ r[1] = short3(w[2]);
+ r[2] = short3(w[3] >> 16);
+ r[3] = short3(w[3]);
+ break;
+ case 5: // FLOAT16_4 (in z, w)
+ r[0] = ppc_half_to_float_bits(w[2] >> 16);
+ r[1] = ppc_half_to_float_bits(w[2] & 0xFFFF);
+ r[2] = ppc_half_to_float_bits(w[3] >> 16);
+ r[3] = ppc_half_to_float_bits(w[3] & 0xFFFF);
+ break;
+ default: // NORMPACKED64 4_20_20_20: not seen yet
+ __builtin_debugtrap();
+ return;
+ }
+ for (int i = 0; i < 4; i++)
+ d.u32[3 - i] = r[i];
+}
+
inline void ppc_vpkd3d128(PPCVRegister& d, const PPCVRegister& b, uint32_t type, uint32_t pack, uint32_t shift)
{
float e[4] = { b.f32[3], b.f32[2], b.f32[1], b.f32[0] };
--
2.50.1 (Apple Git-155)
@@ -0,0 +1,76 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: SpeedBreaker Project <speedbreaker-project@users.noreply.github.com>
Date: Sat, 26 Sep 2026 05:06:04 +0000
Subject: [PATCH] mftb: a 49.875 MHz guest timebase instead of the raw host
counter
The Xbox 360 timebase runs at 49.875 MHz and games measure time with it.
__rdtsc() is 24 MHz on Apple silicon (game time at half speed) and the
TSC's GHz on x86 (game time many times too fast). mftb now emits
PPC_QUERY_TIMEBASE(), by default the host monotonic clock scaled to 49.875 MHz.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
XenonRecomp/recompiler.cpp | 3 ++-
XenonUtils/ppc_context.h | 24 ++++++++++++++++++++++++
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/XenonRecomp/recompiler.cpp b/XenonRecomp/recompiler.cpp
index 147f55d..b75a7ef 100644
--- a/XenonRecomp/recompiler.cpp
+++ b/XenonRecomp/recompiler.cpp
@@ -1264,7 +1264,8 @@ bool Recompiler::Recompile(
break;
case PPC_INST_MFTB:
- println("\t{}.u64 = __rdtsc();", r(insn.operands[0]));
+ // The guest timebase (49.875 MHz on the Xbox 360); see ppc_context.h.
+ println("\t{}.u64 = PPC_QUERY_TIMEBASE();", r(insn.operands[0]));
break;
case PPC_INST_MR:
diff --git a/XenonUtils/ppc_context.h b/XenonUtils/ppc_context.h
index a80eff4..a681071 100644
--- a/XenonUtils/ppc_context.h
+++ b/XenonUtils/ppc_context.h
@@ -5,6 +5,7 @@
#error "ppc_config.h must be included before ppc_context.h"
#endif
+#include <chrono>
#include <climits>
#include <cmath>
#include <csetjmp>
@@ -898,6 +899,29 @@ inline simde__m128i simde_mm_vsr(simde__m128i a, simde__m128i b)
return simde_mm_castps_si128(simde_mm_insert_ps(simde_mm_castsi128_ps(simde_mm_srl_epi64(a, b)), simde_mm_castsi128_ps(simde_mm_srl_epi64(simde_mm_srli_si128(a, 4), b)), 0x10));
}
+// mftb: the Xbox 360's timebase runs at 49.875 MHz (what
+// KeQueryPerformanceFrequency reports), and games time themselves with it.
+// A raw host counter runs at another rate (24 MHz on Apple silicon, the TSC's
+// GHz on x86): game time then runs at the wrong speed. The default scales the
+// host's monotonic clock; define PPC_QUERY_TIMEBASE first to override.
+#ifndef PPC_QUERY_TIMEBASE
+inline uint64_t PpcQueryTimebase()
+{
+ constexpr uint64_t kTimebaseHz = 49875000;
+#if defined(__aarch64__) || defined(_M_ARM64)
+ uint64_t ticks, frequency;
+ asm volatile("mrs %0, cntvct_el0" : "=r"(ticks)::"memory");
+ asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
+ return uint64_t((unsigned __int128)ticks * kTimebaseHz / frequency);
+#else
+ uint64_t ns = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
+ std::chrono::steady_clock::now().time_since_epoch()).count());
+ return uint64_t((unsigned __int128)ns * kTimebaseHz / 1000000000u);
+#endif
+}
+#define PPC_QUERY_TIMEBASE() PpcQueryTimebase()
+#endif
+
#if defined(__aarch64__) || defined(_M_ARM64)
inline uint64_t __rdtsc()
{
--
2.50.1 (Apple Git-155)
+30
View File
@@ -0,0 +1,30 @@
# Compiles XenonRecomp's output (../ppc, generated from the user's own XEX and
# never committed) into a static library. Regenerate ppc/ with:
# tools/XenonRecomp/build/XenonRecomp/XenonRecomp config/nfsmw.toml tools/XenonRecomp/XenonUtils/ppc_context.h
set(ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)
# Built from the top-level CMakeLists.txt.
file(GLOB PPC_SOURCES ${ROOT}/ppc/*.cpp)
if(NOT PPC_SOURCES)
message(FATAL_ERROR "No generated sources in ppc/. Run XenonRecomp first.")
endif()
add_library(NfsmwRecompLib STATIC ${PPC_SOURCES})
target_compile_features(NfsmwRecompLib PUBLIC cxx_std_20)
target_include_directories(NfsmwRecompLib PUBLIC
${ROOT}/ppc
${ROOT}/tools/XenonRecomp/XenonUtils
${ROOT}/tools/XenonRecomp/thirdparty/simde)
target_compile_options(NfsmwRecompLib PRIVATE
-Wno-unused-label -Wno-unused-variable -Wno-unused-but-set-variable
-fno-strict-aliasing
# Checked indirect calls (runtime/cpu/indirect.cpp). SHELL: keeps each
# -include with its file (CMake drops a repeated plain -include).
"SHELL:-include \"${ROOT}/runtime/cpu/indirect.h\""
# mftb reads the game's clock, which stands still while it's suspended.
"SHELL:-include \"${ROOT}/runtime/cpu/guest_time.h\"")
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
target_compile_options(NfsmwRecompLib PRIVATE -march=x86-64-v3)
endif()
if(IOS)
target_compile_options(NfsmwRecompLib PRIVATE -mcpu=apple-a15)
endif()
+156
View File
@@ -0,0 +1,156 @@
# SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
set(XR ${CMAKE_CURRENT_SOURCE_DIR}/../tools/XenonRecomp)
# XEX loading (decryption, LZX) comes from XenonRecomp's XenonUtils; compile
# the few sources we need rather than linking its tool build.
add_library(XenonUtilsLite STATIC
${XR}/XenonUtils/xex.cpp
${XR}/XenonUtils/image.cpp
${XR}/XenonUtils/xex_patcher.cpp
${XR}/XenonUtils/memory_mapped_file.cpp
${XR}/thirdparty/libmspack/libmspack/mspack/lzxd.c
${XR}/thirdparty/tiny-AES-c/aes.c)
target_include_directories(XenonUtilsLite PUBLIC ${XR}/XenonUtils ${XR}/thirdparty/simde
PRIVATE ${XR}/thirdparty/libmspack/libmspack/mspack ${XR}/thirdparty/tiny-AES-c ${XR}/thirdparty/TinySHA1)
target_compile_definitions(XenonUtilsLite PRIVATE NOMINMAX)
target_compile_options(XenonUtilsLite PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-include cstdlib>)
file(GLOB_RECURSE RUNTIME_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
list(FILTER RUNTIME_SOURCES EXCLUDE REGEX "/thirdparty/")
if(APPLE)
# Apple's own glue for macOS and iOS (the thermal state) is
# Objective-C++ on Foundation alone, platform/apple/*.mm. Built with the
# C++ compiler (else CMake picks /usr/bin/c++ next to Homebrew's clang)
# and without ARC, like the rest.
if(NOT CMAKE_OBJCXX_COMPILER)
set(CMAKE_OBJCXX_COMPILER ${CMAKE_CXX_COMPILER})
endif()
enable_language(OBJCXX)
file(GLOB APPLE_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/platform/apple/*.mm)
list(APPEND RUNTIME_SOURCES ${APPLE_SOURCES})
endif()
if(IOS)
# iOS's own glue (the Files app's picker, backups, free space) is
# Objective-C++, platform/*.mm, compiled for iOS only. Built without ARC,
# like the rest; its frameworks (UIKit, Foundation, Metal,
# UniformTypeIdentifiers) are linked by scripts/ios/build.sh.
file(GLOB IOS_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/platform/*.mm)
list(APPEND RUNTIME_SOURCES ${IOS_SOURCES})
endif()
add_library(o1heap STATIC ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/o1heap/o1heap.c)
target_include_directories(o1heap PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/o1heap)
if(IOS)
# On iOS the app is linked and signed by Xcode (scripts/ios): the runtime
# is a static library there, and the app links it with the rest.
add_library(SpeedBreaker STATIC ${RUNTIME_SOURCES})
else()
add_executable(SpeedBreaker ${RUNTIME_SOURCES})
endif()
target_include_directories(SpeedBreaker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/kernel)
find_package(SDL3 REQUIRED CONFIG)
if(IOS)
# iOS has no Vulkan loader: the app links MoltenVK statically, and the
# runtime compiles against MoltenVK's own Vulkan headers.
set(MOLTENVK_DIR "" CACHE PATH "MoltenVK's iOS package (with MoltenVK/include and MoltenVK/static)")
add_library(Vulkan::Vulkan INTERFACE IMPORTED)
target_include_directories(Vulkan::Vulkan INTERFACE ${MOLTENVK_DIR}/MoltenVK/include)
else()
find_package(Vulkan REQUIRED)
endif()
find_package(glslang REQUIRED CONFIG)
# FFmpeg for XMA audio: the minimal static build that scripts/setup_tools.sh
# makes in tools/ffmpeg-xma (with patches/ffmpeg applied: no sample hold-back
# for single-stream XMA, which the game's audio mixer depends on).
set(FFMPEG_XMA ${CMAKE_CURRENT_SOURCE_DIR}/../tools/ffmpeg-xma CACHE PATH "FFmpeg-XMA's install prefix")
find_path(FFMPEG_INCLUDE_DIR libavcodec/avcodec.h PATHS ${FFMPEG_XMA}/include NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED)
find_library(AVCODEC_LIBRARY NAMES libavcodec.a PATHS ${FFMPEG_XMA}/lib NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED)
find_library(AVUTIL_LIBRARY NAMES libavutil.a PATHS ${FFMPEG_XMA}/lib NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH REQUIRED)
# Dear ImGui (thirdparty/imgui, vendored): the installer and the settings overlay.
set(IMGUI ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/imgui)
add_library(imgui STATIC ${IMGUI}/imgui.cpp ${IMGUI}/imgui_draw.cpp ${IMGUI}/imgui_tables.cpp ${IMGUI}/imgui_widgets.cpp
${IMGUI}/backends/imgui_impl_sdl3.cpp ${IMGUI}/backends/imgui_impl_vulkan.cpp)
target_include_directories(imgui PUBLIC ${IMGUI} ${IMGUI}/backends)
target_link_libraries(imgui PUBLIC SDL3::SDL3 Vulkan::Vulkan)
# The UI font, compiled in as a byte array (generated/ui_font.inc).
file(READ ${IMGUI}/fonts/Roboto-Medium.ttf UI_FONT_HEX HEX)
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," UI_FONT_BYTES "${UI_FONT_HEX}")
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated/ui_font.inc "${UI_FONT_BYTES}\n")
target_include_directories(SpeedBreaker PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
# The "What's new" note (ui/whats_new.cpp): this version's entry in
# CHANGELOG.md, from its "## v<version>" heading to the next "## " one,
# compiled in (generated/whats_new.inc). An edit of CHANGELOG.md reconfigures.
set(SB_CHANGELOG ${CMAKE_SOURCE_DIR}/CHANGELOG.md)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${SB_CHANGELOG})
set(SB_WHATS_NEW "")
if(EXISTS ${SB_CHANGELOG})
file(READ ${SB_CHANGELOG} changelog)
string(REPLACE "\r\n" "\n" changelog "\n${changelog}")
string(FIND "${changelog}" "\n## v${CMAKE_PROJECT_VERSION}" at)
if(at GREATER_EQUAL 0)
string(LENGTH "\n## v${CMAKE_PROJECT_VERSION}" heading)
math(EXPR after "${at} + ${heading}")
string(SUBSTRING "${changelog}" ${after} -1 entry)
# The heading's end ("## v0.1.0" or "## v0.1.0 (2026-10-08)", never v0.1.01).
string(REGEX MATCH "^([ \t(][^\n]*)?\n" rest "${entry}")
if(rest STREQUAL "" AND NOT entry STREQUAL "")
set(at -1)
else()
string(LENGTH "${rest}" restLength)
string(SUBSTRING "${entry}" ${restLength} -1 entry)
string(FIND "${entry}" "\n## " next)
if(next GREATER_EQUAL 0)
string(SUBSTRING "${entry}" 0 ${next} entry)
endif()
string(STRIP "${entry}" SB_WHATS_NEW)
endif()
endif()
endif()
if(SB_WHATS_NEW STREQUAL "")
message(WARNING "CHANGELOG.md has no entry for v${CMAKE_PROJECT_VERSION}: this build shows no \"What's new\" note")
endif()
string(FIND "${SB_WHATS_NEW}" ")sbnote\"" bad)
if(bad GREATER_EQUAL 0)
message(FATAL_ERROR "CHANGELOG.md's v${CMAKE_PROJECT_VERSION} entry contains )sbnote\"")
endif()
set(whats_new_inc "// Generated from CHANGELOG.md by runtime/CMakeLists.txt. Do not edit.\nconstexpr const char kWhatsNewEntry[] = R\"sbnote(${SB_WHATS_NEW})sbnote\";\n")
set(whats_new_file ${CMAKE_CURRENT_BINARY_DIR}/generated/whats_new.inc)
set(whats_new_old "")
if(EXISTS ${whats_new_file})
file(READ ${whats_new_file} whats_new_old)
endif()
if(NOT whats_new_old STREQUAL whats_new_inc)
file(WRITE ${whats_new_file} "${whats_new_inc}")
endif()
# The build's identity (version, git commit, dirty flag, build date) for the
# logs, crash reports and Settings > Advanced: generated/build_info.inc,
# checked on every build but rewritten only when it changes
# (report/build_info.cmake), so a no-op build stays one.
add_custom_target(nfsmw_build_info
COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR} -DOUTPUT=${CMAKE_CURRENT_BINARY_DIR}/generated/build_info.inc
-DVERSION=${CMAKE_PROJECT_VERSION} -DBUILD_TYPE=${CMAKE_BUILD_TYPE} -P ${CMAKE_CURRENT_SOURCE_DIR}/report/build_info.cmake
BYPRODUCTS ${CMAKE_CURRENT_BINARY_DIR}/generated/build_info.inc
VERBATIM)
add_dependencies(SpeedBreaker nfsmw_build_info)
target_link_libraries(SpeedBreaker PRIVATE NfsmwRecompLib XenonUtilsLite o1heap imgui SDL3::SDL3 Vulkan::Vulkan glslang::glslang glslang::glslang-default-resource-limits ${AVCODEC_LIBRARY} ${AVUTIL_LIBRARY})
# glslang before 15 (the Steam Frame's 14) keeps the C API's SPIR-V
# generation in its own library.
if(TARGET glslang::SPIRV)
target_link_libraries(SpeedBreaker PRIVATE glslang::SPIRV)
endif()
# platform/apple/*.mm (iOS: linked by scripts/ios/build.sh).
if(APPLE AND NOT IOS)
target_link_libraries(SpeedBreaker PRIVATE "-framework Foundation")
endif()
target_include_directories(SpeedBreaker SYSTEM PRIVATE ${FFMPEG_INCLUDE_DIR} ${XR}/thirdparty/tomlplusplus/include)
# Assertions stay on in every build type: ported code marks its unhandled
# cases with assert(), and those must fail loudly, never fall through.
target_compile_options(SpeedBreaker PRIVATE -Wno-unused-label -Wno-unused-variable -fno-strict-aliasing -UNDEBUG)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
target_compile_options(SpeedBreaker PRIVATE -march=x86-64-v3)
endif()
if(IOS)
# The CPU level both the iPad Pro (M2) and the iPhone 15 Pro (A17 Pro)
# have; Apple clang's iOS default is far older.
target_compile_options(SpeedBreaker PRIVATE -mcpu=apple-a15)
endif()
+395
View File
@@ -0,0 +1,395 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// XAudio render driver. The game registers one client callback; the audio
// system calls it once per 256-sample frame (48 kHz, ~5.33 ms) and the
// callback submits the mixed frame with XAudioSubmitRenderDriverFrame
// (256 samples x 6 channels, planar big-endian float).
//
// Frames go to an SDL3 audio stream (5.1 float; SDL downmixes to whatever
// the device has). The callback runs like the hardware's: one frame every
// 5.33 ms, evenly (the game's voices hold only a couple of frames of
// decoded audio). Calling it in
// bursts of whatever the device pulls at once (1024+ samples on PipeWire)
// left every voice empty for half of each burst: gameplay audio came out
// chopped 50% (two frames on, two off), thin and "tinny". SDL's queue is
// kept near two device buffers to absorb the device's pulls, and the
// cadence gains or skips a frame to follow the device clock. NFSMW_MUTE=1
// (or no audio device) keeps the same cadence with no output. The driver
// handle and wrapped argument follow Xenia.
//
// While the game is suspended (iOS: the app isn't the active one) the
// cadence stands still with guest time and the main thread pauses the
// device (apu::SuspendOutput), which keeps what is queued: on the resume
// that plays first and the cadence carries on at its old phase, with no
// frames owed.
#include <stdafx.h>
#include "audio.h"
#include <kernel/function.h>
#include <cpu/guest_thread.h>
#include <cpu/guest_time.h>
#include <SDL3/SDL.h>
#ifdef __APPLE__
#include <pthread.h>
#endif
#include <user/settings.h>
#include <video/presenter.h>
namespace
{
constexpr uint32_t DRIVER_HANDLE_BASE = 0x41550000; // Xenia
constexpr uint32_t SAMPLES_PER_FRAME = apu::kSamplesPerFrame;
constexpr uint32_t SAMPLE_RATE = apu::kSampleRate;
std::atomic<uint32_t> s_callback{ 0 };
std::atomic<uint32_t> s_callbackArgPtr{ 0 }; // guest pointer to the argument
std::atomic<bool> s_running{ false };
std::atomic<uint64_t> s_framesSubmitted{ 0 };
std::atomic<uint64_t> s_rendered{ 0 }; // render callbacks (apu::RenderedFrames)
constexpr uint32_t CHANNELS = 6; // FL FR FC LFE SL SR
// Set once, under s_streamMutex, which also orders starting the device
// (OpenDevice, a guest thread) against pausing it (the main thread), so
// a suspension can't slip between the check and the start.
std::mutex s_streamMutex;
SDL_AudioStream* s_stream = nullptr;
constexpr int FRAME_BYTES = SAMPLES_PER_FRAME * CHANNELS * sizeof(float);
std::atomic<int> s_targetFrames{ 4 }; // SDL queue level to hold (frames of 256 samples)
int s_deviceFrames = 0; // the device's buffer at the stream's rate (FollowDevice)
// The queue level for the stream's device as it is now: two device
// buffers (at the stream's rate), at least 4 frames. Logs it when it
// changes, as when iOS moves the output to Bluetooth headphones, whose
// buffer can be larger: a target sized for the speaker's then ran dry.
void FollowDevice(bool opening)
{
SDL_AudioSpec deviceSpec{};
int deviceFrames = 0;
SDL_AudioDeviceID device = SDL_GetAudioStreamDevice(s_stream);
if (!SDL_GetAudioDeviceFormat(device, &deviceSpec, &deviceFrames))
return;
if (deviceFrames > 0 && deviceSpec.freq > 0)
deviceFrames = int(int64_t(deviceFrames) * SAMPLE_RATE / deviceSpec.freq);
if (!opening && deviceFrames == s_deviceFrames)
return;
s_deviceFrames = deviceFrames;
s_targetFrames = std::max(4, (2 * deviceFrames + int(SAMPLES_PER_FRAME) - 1) / int(SAMPLES_PER_FRAME));
fprintf(stderr, "[audio] %s: %s, 5.1 float at %u Hz (device %d ch, %s at %d Hz; device buffer %d samples, queue target %.1f ms)\n",
opening ? "output" : "output now", SDL_GetAudioDeviceName(device), SAMPLE_RATE, deviceSpec.channels,
SDL_GetAudioFormatName(deviceSpec.format), deviceSpec.freq, deviceFrames,
1000.0 * s_targetFrames * SAMPLES_PER_FRAME / SAMPLE_RATE);
}
void OpenDevice()
{
// NFSMW_MUTE=1 (automated runs): no device at all. A mute from the
// menu keeps it open at gain 0, so unmuting works live.
if (settings::EnvOverride(settings::Id::Mute) && settings::GetBool(settings::Id::Mute))
return;
if (!SDL_InitSubSystem(SDL_INIT_AUDIO))
{
fprintf(stderr, "[audio] SDL audio init failed: %s (silent)\n", SDL_GetError());
return;
}
SDL_AudioSpec spec{ SDL_AUDIO_F32, int(CHANNELS), int(SAMPLE_RATE) };
SDL_AudioStream* stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
if (!stream)
{
fprintf(stderr, "[audio] no playback device: %s (silent)\n", SDL_GetError());
return;
}
{
// It opens paused. Opened while the game is suspended, it stays
// so until apu::ResumeOutput starts it.
std::lock_guard lock(s_streamMutex);
s_stream = stream;
if (!guesttime::Suspended())
SDL_ResumeAudioStreamDevice(s_stream);
}
FollowDevice(true);
}
void RenderFrame(GuestThreadContext& ctx)
{
uint32_t callback = s_callback;
if (callback != 0)
{
ctx.ppcContext.r3.u64 = s_callbackArgPtr;
g_memory.FindFunction(callback)(ctx.ppcContext, g_memory.base);
}
}
void AudioThread()
{
using namespace std::chrono;
#ifdef __APPLE__
// Every 5.3 ms, or the queue runs dry and the output crackles. At the
// default QoS an iPhone (two performance cores, both busy with the
// game and the command processor) ran it late on an efficiency core.
pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
#endif
GuestThreadContext ctx(0);
SetHostThreadName("nfsmw-audio");
constexpr auto INTERVAL = nanoseconds(1000000000ull * SAMPLES_PER_FRAME / SAMPLE_RATE);
// The time suspended that `next` allows for. Both taken while running
// (XAudio can start this thread in a suspension): `next` is a host
// time at which `paused` held.
steady_clock::time_point next;
int64_t paused;
do
{
guesttime::WaitWhileSuspended();
paused = guesttime::PausedNs();
next = steady_clock::now();
} while (guesttime::Suspended() || guesttime::PausedNs() != paused);
while (s_running)
{
std::this_thread::sleep_until(next);
// No guest audio time passes while the game is suspended: render
// nothing (the main thread has paused the device). After it the
// cadence moves on by exactly the time suspended, keeping its
// phase in guest time (the movies' clock follows the audio
// played): no frame is owed, and the queue is still at its
// level, so there is no burst either.
guesttime::WaitWhileSuspended();
if (int64_t nowPaused = guesttime::PausedNs(); nowPaused != paused)
{
next += nanoseconds(nowPaused - paused);
paused = nowPaused;
continue;
}
next += INTERVAL;
auto now = steady_clock::now();
if (next < now - milliseconds(100))
next = now; // fell far behind (debugger, stall): don't burst
int frames = 1;
if (s_stream)
{
// The device can change under the stream (headphones on or
// off): its buffer sets the target, so check it now and then.
static uint32_t sinceCheck = 0;
if (++sinceCheck >= SAMPLE_RATE / SAMPLES_PER_FRAME) // ~1 s
{
sinceCheck = 0;
FollowDevice(false);
}
// Follow the device clock: one frame more or less when the
// queue drifts from its target.
int queued = SDL_GetAudioStreamQueued(s_stream) / FRAME_BYTES;
if (queued > s_targetFrames + 2)
frames = 0;
else if (queued < s_targetFrames - 2)
frames = 2;
}
for (int i = 0; i < frames && !guesttime::Suspended(); i++)
{
RenderFrame(ctx);
s_rendered.fetch_add(1, std::memory_order_relaxed);
}
}
}
}
namespace apu
{
void SuspendOutput()
{
std::lock_guard lock(s_streamMutex);
if (s_stream)
SDL_PauseAudioStreamDevice(s_stream);
}
void ResumeOutput()
{
std::lock_guard lock(s_streamMutex);
if (s_stream && !guesttime::Suspended())
SDL_ResumeAudioStreamDevice(s_stream);
}
uint64_t RenderedFrames()
{
return s_rendered.load(std::memory_order_relaxed);
}
double QueuedMs()
{
std::lock_guard lock(s_streamMutex);
if (!s_stream)
return 0.0;
int queued = std::max(0, SDL_GetAudioStreamQueued(s_stream));
return 1000.0 * queued / (CHANNELS * sizeof(float) * SAMPLE_RATE);
}
}
uint32_t XAudioRegisterRenderDriverClient(be<uint32_t>* callback, be<uint32_t>* driver)
{
// callback[0] = function, callback[1] = argument. Like Xenia, the callback
// receives a pointer to a guest copy of the argument.
uint32_t function = callback[0];
uint32_t argument = callback[1];
auto* argCopy = static_cast<be<uint32_t>*>(g_userHeap.Alloc(sizeof(uint32_t)));
*argCopy = argument;
if (s_callback != 0)
{
fprintf(stderr, "[audio] a second render driver client is not supported yet\n");
assert(false && "multiple XAudio render clients");
}
s_callbackArgPtr = g_memory.MapVirtual(argCopy);
s_callback = function;
*driver = DRIVER_HANDLE_BASE;
fprintf(stderr, "[audio] render client %08X (arg %08X) registered\n", function, argument);
if (!s_running.exchange(true))
{
OpenDevice();
std::thread(AudioThread).detach();
}
return 0;
}
uint32_t XAudioUnregisterRenderDriverClient(uint32_t driver)
{
s_callback = 0;
return 0;
}
// 256 samples x 6 channels, planar, big-endian float.
uint32_t XAudioSubmitRenderDriverFrame(uint32_t driver, void* samples)
{
s_framesSubmitted++;
if (!s_stream || !samples)
return 0;
// Volume, mute and mute-in-background, when they or the focus change.
{
static uint64_t generation = ~0ull;
static bool wasFocused = true;
bool focused = video::WindowFocused();
if (uint64_t g = settings::Generation(); g != generation || focused != wasFocused)
{
generation = g;
wasFocused = focused;
bool muted = settings::GetBool(settings::Id::Mute);
bool background = !focused && settings::GetBool(settings::Id::MuteInBackground);
float volume = float(settings::GetInt(settings::Id::MasterVolume)) / 100.0f;
float gain = muted || background ? 0.0f : volume * volume; // squared: closer to perceived loudness
SDL_SetAudioStreamGain(s_stream, gain);
static float logged = -1.0f;
if (gain != logged)
{
// With the output position, so a recording's level can be matched to it.
logged = gain;
fprintf(stderr, "[audio] gain %.4f (volume %d%%%s%s) from %.2f s of output\n", gain, int(volume * 100.0f + 0.5f),
muted ? ", muted" : "", background ? ", muted in the background" : "",
double(s_framesSubmitted) * SAMPLES_PER_FRAME / SAMPLE_RATE);
}
}
}
const auto* in = static_cast<const uint32_t*>(samples);
float out[SAMPLES_PER_FRAME * CHANNELS];
for (uint32_t c = 0; c < CHANNELS; c++)
for (uint32_t i = 0; i < SAMPLES_PER_FRAME; i++)
{
uint32_t bits = ByteSwap(in[c * SAMPLES_PER_FRAME + i]);
float v;
memcpy(&v, &bits, 4);
out[i * CHANNELS + c] = v;
}
// Cut-out detection: the device ran dry, or the game's frames stopped.
// Gaps in guest time, so a suspension (the device paused with it) isn't
// one, even for a frame that was under way as it began.
{
static int64_t lastFrame = guesttime::NowNs();
int64_t guestNow = guesttime::NowNs();
double gapMs = double(guestNow - lastFrame) / 1e6;
lastFrame = guestNow;
auto now = std::chrono::steady_clock::now();
int queued = SDL_GetAudioStreamQueued(s_stream);
static uint32_t underruns = 0, gaps = 0;
static double worstGap = 0;
if (queued == 0)
underruns++;
if (gapMs > 50)
gaps++;
worstGap = std::max(worstGap, gapMs);
// Every 2 s with NFSMW_AUDIO_LOG; otherwise only when it went wrong,
// so a player's log shows where a crackle came from.
static const bool logStats = std::getenv("NFSMW_AUDIO_LOG") != nullptr;
static auto nextLog = now + std::chrono::seconds(2);
if (now >= nextLog && (logStats || underruns || gaps))
{
fprintf(stderr, "[audio] frames: %u device-empty on submit, %u gaps > 50 ms (worst %.0f ms)\n", underruns, gaps, worstGap);
underruns = gaps = 0;
worstGap = 0;
nextLog = now + std::chrono::seconds(2);
}
}
SDL_PutAudioStreamData(s_stream, out, sizeof(out));
// NFSMW_AUDIO_DUMP=<file>: the final mix as raw interleaved 6-channel
// float (48 kHz), from NFSMW_AUDIO_DUMP_FROM seconds for
// NFSMW_AUDIO_DUMP_SECONDS (default 30).
{
static const char* dumpPath = std::getenv("NFSMW_AUDIO_DUMP");
static const double from = [] { const char* v = std::getenv("NFSMW_AUDIO_DUMP_FROM"); return v ? std::atof(v) : 0.0; }();
static const double seconds = [] { const char* v = std::getenv("NFSMW_AUDIO_DUMP_SECONDS"); return v ? std::atof(v) : 30.0; }();
static const auto start = std::chrono::steady_clock::now();
static FILE* dump = nullptr;
static uint64_t dumped = 0;
if (dumpPath && dumped < uint64_t(seconds * SAMPLE_RATE) &&
std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count() >= from)
{
if (!dump && !(dump = fopen(dumpPath, "wb")))
dumpPath = nullptr;
if (dump)
{
fwrite(out, sizeof(out), 1, dump);
dumped += SAMPLES_PER_FRAME;
if (dumped >= uint64_t(seconds * SAMPLE_RATE))
{
fclose(dump);
fprintf(stderr, "[audio] dump written: %s\n", dumpPath);
}
}
}
}
// NFSMW_AUDIO_LOG=1: level meter every ~2 s.
static const bool logLevels = std::getenv("NFSMW_AUDIO_LOG") != nullptr;
if (logLevels)
{
static double sumSquares = 0;
static float peak = 0;
static uint32_t frames = 0;
for (float v : out)
{
sumSquares += double(v) * v;
peak = std::max(peak, std::fabs(v));
}
if (++frames == 375)
{
fprintf(stderr, "[audio] level: peak %.3f, rms %.4f\n", peak,
std::sqrt(sumSquares / (375.0 * SAMPLES_PER_FRAME * CHANNELS)));
sumSquares = 0;
peak = 0;
frames = 0;
}
}
return 0;
}
uint32_t XAudioGetVoiceCategoryVolumeChangeMask(uint32_t driver, be<uint32_t>* mask)
{
*mask = 0;
return 0;
}
uint32_t XAudioGetVoiceCategoryVolume(uint32_t category, be<float>* volume)
{
*volume = 1.0f;
return 0;
}
GUEST_FUNCTION_HOOK(__imp__XAudioRegisterRenderDriverClient, XAudioRegisterRenderDriverClient);
GUEST_FUNCTION_HOOK(__imp__XAudioUnregisterRenderDriverClient, XAudioUnregisterRenderDriverClient);
GUEST_FUNCTION_HOOK(__imp__XAudioSubmitRenderDriverFrame, XAudioSubmitRenderDriverFrame);
GUEST_FUNCTION_HOOK(__imp__XAudioGetVoiceCategoryVolumeChangeMask, XAudioGetVoiceCategoryVolumeChangeMask);
GUEST_FUNCTION_HOOK(__imp__XAudioGetVoiceCategoryVolume, XAudioGetVoiceCategoryVolume);
+27
View File
@@ -0,0 +1,27 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The XAudio render driver (audio.cpp), as the rest of the runtime sees it
// while the game is suspended (iOS: the app isn't the active one; see
// cpu/guest_time.h).
#pragma once
#include <cstdint>
namespace apu
{
// Main thread (video's SuspendGame and ResumeGame), idempotent. Suspend
// pauses the output device: it plays silence without taking what is
// queued, which then plays first on the resume. Resume starts it again,
// after guesttime::Resume(): never while the game is still suspended.
// The render cadence stops and starts with guest time on its own.
void SuspendOutput();
void ResumeOutput();
// A render callback's frame: 256 samples at 48 kHz.
inline constexpr uint32_t kSamplesPerFrame = 256, kSampleRate = 48000;
inline constexpr double kFrameNs = kSamplesPerFrame * 1e9 / kSampleRate; // 5.33 ms
// Any thread: render callbacks so far (one frame of guest time each),
// and the audio queued for the device (ms; 0 without one).
uint64_t RenderedFrames();
double QueuedMs();
}
+1068
View File
File diff suppressed because it is too large. Load diff
+264
View File
@@ -0,0 +1,264 @@
// Adapted from Unleashed Recompiled (https://github.com/hedge-dev/UnleashedRecomp),
// GPL-3.0-or-later. Modified for SpeedBreaker.
#include <stdafx.h>
#include "guest_thread.h"
#include <kernel/memory.h>
#include <kernel/heap.h>
#include <kernel/function.h>
#include <cpu/ppc_context.h>
#include <report/report.h>
#include <sys/resource.h>
constexpr size_t PCR_SIZE = 0xAB0;
constexpr size_t TLS_SIZE = 0x100;
constexpr size_t TEB_SIZE = 0x2E0;
// SpeedBreaker: guest stack size comes from the XEX (SetDefaultGuestStackSize).
static size_t STACK_SIZE = 0x40000;
void GuestThread::SetDefaultStackSize(size_t size) { STACK_SIZE = size; }
#define TOTAL_SIZE (PCR_SIZE + TLS_SIZE + TEB_SIZE + STACK_SIZE)
constexpr size_t TEB_OFFSET = PCR_SIZE + TLS_SIZE;
// SpeedBreaker: small sequential ids (no xxHash). Each handle owns its id and
// the new host thread adopts it, so the creator and the thread agree.
static std::atomic<uint32_t> s_nextThreadId{ 1 };
static thread_local uint32_t t_threadId = 0;
static thread_local KernelObject* t_self = nullptr;
// Threads not created through ExCreateThread (the main thread) get a plain
// object: nothing can wait for them to finish.
struct ForeignThreadObject final : Waitable
{
bool IsSignaledLocked() const override { return false; }
};
KernelObject* GuestThread::GetCurrentThreadObject()
{
if (t_self == nullptr)
t_self = CreateKernelObject<ForeignThreadObject>();
return t_self;
}
static uint32_t CurrentThreadId()
{
if (t_threadId == 0)
t_threadId = s_nextThreadId++;
return t_threadId;
}
GuestThreadContext::GuestThreadContext(uint32_t cpuNumber)
{
assert(thread == nullptr);
thread = (uint8_t*)g_userHeap.Alloc(TOTAL_SIZE);
memset(thread, 0, TOTAL_SIZE);
*(uint32_t*)thread = ByteSwap(g_memory.MapVirtual(thread + PCR_SIZE)); // tls pointer
*(uint32_t*)(thread + 0x100) = ByteSwap(g_memory.MapVirtual(thread + PCR_SIZE + TLS_SIZE)); // teb pointer
*(thread + 0x10C) = cpuNumber;
*(uint32_t*)(thread + PCR_SIZE + 0x10) = 0xFFFFFFFF; // that one TLS entry that felt quirky
*(uint32_t*)(thread + PCR_SIZE + TLS_SIZE + 0x14C) = ByteSwap(GuestThread::GetCurrentThreadId()); // thread id
ppcContext.r1.u64 = g_memory.MapVirtual(thread + PCR_SIZE + TLS_SIZE + TEB_SIZE + STACK_SIZE); // stack pointer
ppcContext.r13.u64 = g_memory.MapVirtual(thread);
ppcContext.fpscr.loadFromHost();
assert(GetPPCContext() == nullptr);
SetPPCContext(ppcContext);
report::SetThreadGuestContext(&ppcContext); // for crash and hang reports
}
GuestThreadContext::~GuestThreadContext()
{
report::SetThreadGuestContext(nullptr);
g_userHeap.Free(thread);
}
#ifdef USE_PTHREAD
static size_t GetStackSize()
{
// Cache as this should not change.
static size_t stackSize = 0;
if (stackSize == 0)
{
// 8 MiB is a typical default.
constexpr auto defaultSize = 8 * 1024 * 1024;
struct rlimit lim;
const auto ret = getrlimit(RLIMIT_STACK, &lim);
#if defined(__APPLE__) && TARGET_OS_IOS
// iOS limits only the main thread's stack (1 MiB); a thread made
// here may have 8 like everywhere else.
if (false)
#else
if (ret == 0 && lim.rlim_cur < defaultSize)
#endif
{
// Use what the system allows.
stackSize = lim.rlim_cur;
}
else
{
stackSize = defaultSize;
}
}
return stackSize;
}
void SetHostThreadName(const char* name)
{
#if defined(__APPLE__)
pthread_setname_np(name);
#elif defined(__linux__)
pthread_setname_np(pthread_self(), name);
#else
(void)name;
#endif
report::RegisterThread(name); // hang reports sample named threads
}
static void* GuestThreadFunc(void* arg)
{
GuestThreadHandle* hThread = (GuestThreadHandle*)arg;
#else
static void GuestThreadFunc(GuestThreadHandle* hThread)
{
#endif
t_threadId = hThread->id;
t_self = hThread;
{
char name[16];
snprintf(name, sizeof(name), "g%u-%08X", hThread->id, hThread->params.function);
SetHostThreadName(name);
}
hThread->suspended.wait(true);
GuestThread::Start(hThread->params);
// SpeedBreaker: signal waiters, then drop the thread's own reference.
{
std::lock_guard lock(dispatcher::Lock());
hThread->finished = true;
dispatcher::NotifyAllLocked();
}
ReleaseKernelObject(hThread);
#ifdef USE_PTHREAD
return nullptr;
#endif
}
GuestThreadHandle::GuestThreadHandle(const GuestThreadParams& params)
: params(params), id(s_nextThreadId++), suspended((params.flags & 0x1) != 0)
#ifdef USE_PTHREAD
{
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, GetStackSize());
refs = 2; // SpeedBreaker: the handle's reference + the thread's own
const auto ret = pthread_create(&thread, &attr, GuestThreadFunc, this);
if (ret != 0) {
fprintf(stderr, "pthread_create failed with error code 0x%X.\n", ret);
return;
}
pthread_detach(thread);
}
#else
, thread(GuestThreadFunc, this)
{
}
#endif
// SpeedBreaker: threads are detached at creation; closing the last handle
// must never block on (or kill) a running thread.
GuestThreadHandle::~GuestThreadHandle()
{
}
uint32_t GuestThreadHandle::GetThreadId() const
{
return id;
}
uint32_t GuestThread::Start(const GuestThreadParams& params)
{
const auto procMask = (uint8_t)(params.flags >> 24);
const auto cpuNumber = procMask == 0 ? 0 : 7 - std::countl_zero(procMask);
GuestThreadContext ctx(cpuNumber);
ctx.ppcContext.r3.u64 = params.value;
ctx.ppcContext.r4.u64 = params.value2;
try
{
g_memory.FindFunction(params.function)(ctx.ppcContext, g_memory.base);
}
catch (const GuestThreadExit& exit)
{
ctx.ppcContext.r3.u64 = exit.exitCode;
}
return ctx.ppcContext.r3.u32;
}
GuestThreadHandle* GuestThread::Start(const GuestThreadParams& params, uint32_t* threadId)
{
auto hThread = CreateKernelObject<GuestThreadHandle>(params);
if (threadId != nullptr)
{
*threadId = hThread->GetThreadId();
}
return hThread;
}
uint32_t GuestThread::GetCurrentThreadId()
{
return CurrentThreadId();
}
void GuestThread::SetLastError(uint32_t error)
{
auto* thread = (char*)g_memory.Translate(GetPPCContext()->r13.u32);
if (*(uint32_t*)(thread + 0x150))
{
// Program doesn't want errors
return;
}
// TEB + 0x160 : Win32LastError
*(uint32_t*)(thread + TEB_OFFSET + 0x160) = ByteSwap(error);
}
#ifdef _WIN32
void GuestThread::SetThreadName(uint32_t threadId, const char* name)
{
#pragma pack(push,8)
const DWORD MS_VC_EXCEPTION = 0x406D1388;
typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // Must be 0x1000.
LPCSTR szName; // Pointer to name (in user addr space).
DWORD dwThreadID; // Thread ID (-1=caller thread).
DWORD dwFlags; // Reserved for future use, must be zero.
} THREADNAME_INFO;
#pragma pack(pop)
THREADNAME_INFO info;
info.dwType = 0x1000;
info.szName = name;
info.dwThreadID = threadId;
info.dwFlags = 0;
__try
{
RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
}
}
#endif
+81
View File
@@ -0,0 +1,81 @@
// Adapted from Unleashed Recompiled (https://github.com/hedge-dev/UnleashedRecomp),
// GPL-3.0-or-later. Modified for SpeedBreaker.
#pragma once
#include <kernel/xdm.h>
#include <kernel/dispatcher.h>
// SpeedBreaker: pthreads on every platform (macOS and Linux), for the stack
// size and so thread handles share one detached, reference-counted path.
#define USE_PTHREAD 1
#ifdef USE_PTHREAD
#include <pthread.h>
#endif
#define CURRENT_THREAD_HANDLE uint32_t(-2)
struct GuestThreadContext
{
PPCContext ppcContext{};
uint8_t* thread = nullptr;
GuestThreadContext(uint32_t cpuNumber);
~GuestThreadContext();
};
// Thrown by ExTerminateThread: unwinds the guest call stack back to
// GuestThread::Start (the 360 call never returns).
struct GuestThreadExit
{
uint32_t exitCode;
};
struct GuestThreadParams
{
uint32_t function;
uint32_t value; // r3
uint32_t flags;
uint32_t value2 = 0; // r4 (SpeedBreaker: XAPI thread startup takes two arguments)
};
struct GuestThreadHandle : Waitable
{
GuestThreadParams params;
uint32_t id;
std::atomic<bool> suspended;
bool finished = false; // SpeedBreaker: guarded by dispatcher::Lock()
#ifdef USE_PTHREAD
pthread_t thread;
#else
std::thread thread;
#endif
GuestThreadHandle(const GuestThreadParams& params);
~GuestThreadHandle() override;
uint32_t GetThreadId() const;
// SpeedBreaker: signaled once the thread has finished.
bool IsSignaledLocked() const override { return finished; }
};
struct GuestThread
{
static uint32_t Start(const GuestThreadParams& params);
static GuestThreadHandle* Start(const GuestThreadParams& params, uint32_t* threadId);
static uint32_t GetCurrentThreadId();
static void SetLastError(uint32_t error);
static void SetDefaultStackSize(size_t size);
// The current thread's kernel object (for the 0xFFFFFFFE pseudo-handle).
static KernelObject* GetCurrentThreadObject();
#ifdef _WIN32
static void SetThreadName(uint32_t threadId, const char* name);
#endif
};
// Names the calling host thread, for top -H, perf and debuggers (at most 15
// characters are kept on Linux).
void SetHostThreadName(const char* name);
+179
View File
@@ -0,0 +1,179 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See guest_time.h.
// Standard headers only, so a test can build it alone.
#include "guest_time.h"
#include <condition_variable>
#include <mutex>
#include <thread>
namespace guesttime
{
State g_state;
namespace
{
std::mutex s_mutex; // a leaf: nothing else is taken while it's held
// Never destroyed: threads stay blocked on them for good (the hook
// thread, a waiter parked in a suspension), and glibc's
// pthread_cond_destroy waits for blocked waiters, so an exit through
// main's return would hang (as video/presenter.cpp's timing waiter did).
std::condition_variable& s_cv = *new std::condition_variable;
std::atomic<uint64_t> s_epoch{ 0 };
std::atomic<int64_t> s_lastResume{ 0 };
// The resume hook's thread: Resume() only bumps s_resumes and
// notifies; this thread then calls the hook, which takes the
// kernel's locks (held by game threads, possibly across a write
// fault waiting for the GPU) where UIKit's callback mustn't wait.
void (*s_hook)() = nullptr;
uint64_t s_resumes = 0, s_hooked = 0; // under s_mutex
std::condition_variable& s_hookCv = *new std::condition_variable;
// A reader's counter read isn't ordered with the loads around it, so
// it can land a little after the writer's own: each transition moves
// guest time on by this much, so it never runs backwards (game code
// subtracts mftb and the tick count unsigned).
constexpr int64_t kMarginTicks = 50, kMarginNs = 1000, kMargin100ns = 10;
template<class F>
void Write(F change)
{
uint64_t seq = g_state.seq.load(std::memory_order_relaxed);
g_state.seq.store(seq + 1, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_seq_cst); // odd before any clock is read
#if defined(__aarch64__) || defined(_M_ARM64)
// A fence orders memory accesses only: mrs cntvct_el0 could read
// the counter before the odd count was visible, and a reader still
// passing its check then got a later mftb than the frozen one (up
// to 3.8 us back, tests/guest_time_test.cpp). dsb waits for the
// store, isb keeps the counter read after it.
asm volatile("dsb ish\n\tisb" ::: "memory");
#endif
change();
g_state.seq.store(seq + 2, std::memory_order_release);
}
template<class T>
T ReadConsistent(T (*get)())
{
while (true)
{
uint64_t seq = g_state.seq.load(std::memory_order_acquire);
if (seq & 1)
continue;
T v = get();
std::atomic_thread_fence(std::memory_order_acquire);
if (g_state.seq.load(std::memory_order_relaxed) == seq)
return v;
}
}
void HookThread()
{
std::unique_lock lock(s_mutex);
while (true)
{
s_hookCv.wait(lock, [] { return s_hooked != s_resumes; });
s_hooked = s_resumes;
void (*hook)() = s_hook;
lock.unlock();
if (hook)
hook();
lock.lock();
}
}
}
bool Suspended()
{
return ReadConsistent<bool>([] { return g_state.suspended.load(std::memory_order_relaxed); });
}
int64_t PausedNs()
{
return ReadConsistent<int64_t>([] { return g_state.ns.offset.load(std::memory_order_relaxed); });
}
uint64_t Epoch()
{
return s_epoch.load(std::memory_order_acquire);
}
int64_t LastResumeNs()
{
return s_lastResume.load(std::memory_order_acquire);
}
bool Suspend()
{
std::lock_guard lock(s_mutex);
if (g_state.suspended.load(std::memory_order_relaxed))
return false;
// The epoch first: a thread that parks anywhere in this suspension
// sees it changed once it's back, whichever it checks.
s_epoch.fetch_add(1, std::memory_order_acq_rel);
Write([] {
auto freeze = [](Domain& d, int64_t raw, int64_t margin) {
d.frozen.store(raw - d.offset.load(std::memory_order_relaxed) + margin, std::memory_order_relaxed);
};
freeze(g_state.timebase, RawTimebase(), kMarginTicks);
freeze(g_state.ns, RawNs(), kMarginNs);
freeze(g_state.system, RawSystem(), kMargin100ns);
g_state.suspended.store(true, std::memory_order_release);
});
// The hook's thread, from the first suspension: a run that never
// suspends (every desktop run) has the threads it always had.
// Creating it waits on nothing. It finds s_resumes ahead of s_hooked
// even if the Resume comes first.
static std::once_flag started;
std::call_once(started, [] { std::thread(HookThread).detach(); });
return true;
}
bool Resume()
{
{
std::lock_guard lock(s_mutex);
if (!g_state.suspended.load(std::memory_order_relaxed))
return false;
Write([] {
auto thaw = [](Domain& d, int64_t raw, int64_t margin) {
d.offset.store(raw - d.frozen.load(std::memory_order_relaxed) - margin, std::memory_order_relaxed);
};
thaw(g_state.timebase, RawTimebase(), kMarginTicks);
thaw(g_state.ns, RawNs(), kMarginNs);
thaw(g_state.system, RawSystem(), kMargin100ns);
g_state.suspended.store(false, std::memory_order_release);
});
s_lastResume.store(RawNs(), std::memory_order_release);
s_resumes++;
}
s_cv.notify_all();
s_hookCv.notify_all();
return true;
}
void WaitWhileSuspendedSlow()
{
std::unique_lock lock(s_mutex);
s_cv.wait(lock, [] { return !g_state.suspended.load(std::memory_order_relaxed); });
}
void SleepUntil(int64_t deadline)
{
while (true)
{
WaitWhileSuspended();
int64_t left = deadline - NowNs();
if (left <= 0)
return;
std::this_thread::sleep_for(std::chrono::nanoseconds(left));
}
}
void SetResumeHook(void (*hook)())
{
std::lock_guard lock(s_mutex);
s_hook = hook;
}
}
+138
View File
@@ -0,0 +1,138 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The game's clocks, which stand still while the game is suspended (iOS: the
// app isn't the active one, from Control Center to the background). Every
// time the guest can observe comes from here or waits on it: the timebase
// (mftb, its QueryPerformanceCounter), the system time, the tick count, wait
// timeouts, kernel timers, Sleep, the vblank interrupt and the audio render
// cadence. Guest time is host time minus the time spent suspended, so after
// a suspension every clock carries on from where it stopped and nothing
// catches up (on 2026-10-02 the intro movie ran at 2x after Control Center:
// only the GPU had stopped).
//
// Nothing suspends on desktop (SDL sends the app events only on iOS and
// Android; NFSMW_TEST_SUSPEND does in tests): the offsets stay 0 and every
// reading is the host's, as before.
//
// Force-included into the recompiled code ahead of ppc_context.h, whose
// PPC_QUERY_TIMEBASE this replaces, so it uses standard headers only.
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
namespace guesttime
{
// A guest clock: guest = raw - offset, or frozen while suspended.
struct Domain
{
std::atomic<int64_t> offset{ 0 };
std::atomic<int64_t> frozen{ 0 };
};
// One seqlock over every domain, so a reader never mixes a clock's old
// offset with the new suspended flag.
struct State
{
std::atomic<uint64_t> seq{ 0 }; // odd while Suspend/Resume rewrite the rest
std::atomic<bool> suspended{ false };
Domain timebase; // 49.875 MHz ticks (mftb)
Domain ns; // steady_clock ns: timeouts, timers, Sleep, the tick count, vblank, audio
Domain system; // 100 ns units since 1601 (KeQuerySystemTime)
};
extern State g_state;
// The 360's timebase rate (what KeQueryPerformanceFrequency reports).
inline constexpr uint64_t kTimebaseHz = 49875000;
// The host clocks as the runtime read them before (ppc_context.h's
// PpcQueryTimebase for the timebase).
inline int64_t RawTimebase()
{
#if defined(__aarch64__) || defined(_M_ARM64)
uint64_t ticks, frequency;
asm volatile("mrs %0, cntvct_el0" : "=r"(ticks)::"memory");
asm volatile("mrs %0, cntfrq_el0" : "=r"(frequency));
return int64_t((unsigned __int128)ticks * kTimebaseHz / frequency);
#else
uint64_t ns = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
return int64_t((unsigned __int128)ns * kTimebaseHz / 1000000000u);
#endif
}
inline int64_t RawNs()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
inline int64_t RawSystem()
{
constexpr int64_t kEpochDelta = 116444736000000000; // 1601 to 1970 in 100 ns
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count() / 100 +
kEpochDelta;
}
template<int64_t (*Raw)(), Domain State::*D>
inline int64_t Read()
{
while (true)
{
uint64_t seq = g_state.seq.load(std::memory_order_acquire);
if (seq & 1)
continue; // a transition, a few us
const Domain& d = g_state.*D;
int64_t v = g_state.suspended.load(std::memory_order_relaxed) ? d.frozen.load(std::memory_order_relaxed)
: Raw() - d.offset.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (g_state.seq.load(std::memory_order_relaxed) == seq)
return v;
}
}
// Guest time in each domain.
inline int64_t Timebase() { return Read<RawTimebase, &State::timebase>(); }
inline int64_t NowNs() { return Read<RawNs, &State::ns>(); }
inline int64_t SystemTime100ns() { return Read<RawSystem, &State::system>(); }
// Whether the game is suspended, and the ns spent suspended in all so far
// (constant while suspended, it changes at Resume: grows, or after a
// suspension under 2 us drops by the margins). Each reads through the
// seqlock, so it agrees with the clocks at its own instant; two calls
// can straddle a transition.
bool Suspended();
int64_t PausedNs();
uint64_t Epoch(); // Suspend() calls that took effect so far
int64_t LastResumeNs(); // host steady ns of the last Resume (0: none yet)
// Any thread holding no lock the main thread could need: returns once the
// game isn't suspended.
void WaitWhileSuspendedSlow();
inline void WaitWhileSuspended()
{
if (g_state.suspended.load(std::memory_order_acquire))
WaitWhileSuspendedSlow();
}
// Sleeps until guest time `deadline` (ns), longer by any suspension.
void SleepUntil(int64_t deadline);
// The main thread (iOS: inside UIKit's scene callbacks), idempotent:
// UIKit reports each transition twice. True when it changed something.
// Neither waits on a game thread nor takes a game lock: Resume wakes
// the kernel's waiters from a thread of its own (SetResumeHook).
bool Suspend();
bool Resume();
// Called after every Resume, on a helper thread (never Resume's caller),
// to wake waits parked without a timeout while suspended.
void SetResumeHook(void (*hook)());
}
// The recompiled code's mftb (runtime files that include ppc_context.h
// first keep its default, which they don't use).
inline uint64_t GuestTimebase()
{
return uint64_t(guesttime::Timebase());
}
#ifndef PPC_QUERY_TIMEBASE
#define PPC_QUERY_TIMEBASE() GuestTimebase()
#endif
+158
View File
@@ -0,0 +1,158 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
#include <stdafx.h>
#include "host_cpu.h"
#ifdef __linux__
#include <sched.h>
#include <sys/prctl.h>
#endif
namespace hostcpu
{
#ifdef __linux__
namespace
{
// Set by ReserveFastCore before any other thread exists, read-only after.
bool s_reserving = false;
cpu_set_t s_reserved, s_shared;
uint64_t ReadCpuValue(int cpu, const char* file)
{
char path[96];
snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/%s", cpu, file);
FILE* f = fopen(path, "r");
if (!f)
return 0;
unsigned long long value = 0;
if (fscanf(f, "%llu", &value) != 1)
value = 0;
fclose(f);
return value;
}
std::string CpuList(const cpu_set_t& set)
{
std::string list;
for (int cpu = 0; cpu < CPU_SETSIZE; cpu++)
if (CPU_ISSET(cpu, &set))
list += (list.empty() ? "" : ",") + std::to_string(cpu);
return list;
}
}
#endif
void ReserveFastCore()
{
#ifdef __linux__
// NFSMW_CP_OWN_CORE=0: never; =fast: only on CPUs with fast and slow
// cores (the rule until 2026-09-29, when alike cores got one too).
const char* mode = std::getenv("NFSMW_CP_OWN_CORE");
if (mode && mode[0] == '0')
return;
bool fastOnly = mode && strcmp(mode, "fast") == 0;
cpu_set_t allowed;
if (sched_getaffinity(0, sizeof(allowed), &allowed) != 0)
return;
// Each CPU's top speed: cpuinfo_max_freq (Intel's hybrid cores), or
// where that is the same on every core, as amd-pstate reports it, the
// CPPC highest performance (196 on two of the Steam Machine's cores,
// 144 on the other four; 255 on each of the Deck's). A file that reads
// 0 everywhere isn't there: the other one tells.
struct Cpu { int cpu; uint64_t speed, core, capacity; };
std::vector<Cpu> cpus, read;
for (const char* file : { "cpufreq/cpuinfo_max_freq", "acpi_cppc/highest_perf" })
{
read.clear();
for (int cpu = 0; cpu < CPU_SETSIZE; cpu++)
if (CPU_ISSET(cpu, &allowed))
read.push_back({ cpu, ReadCpuValue(cpu, file),
ReadCpuValue(cpu, "topology/physical_package_id") << 32 | ReadCpuValue(cpu, "topology/core_id"),
ReadCpuValue(cpu, "cpu_capacity") });
if (std::all_of(read.begin(), read.end(), [](const Cpu& c) { return c.speed == 0; }))
continue;
cpus = read;
if (std::any_of(cpus.begin(), cpus.end(), [&](const Cpu& c) { return c.speed != cpus[0].speed; }))
break;
}
if (cpus.empty())
return;
auto [low, high] = std::minmax_element(cpus.begin(), cpus.end(), [](const Cpu& a, const Cpu& b) { return a.speed < b.speed; });
if (low->speed == 0)
return;
std::vector<uint64_t> cores, fastCores;
for (const Cpu& c : cpus)
{
if (std::find(cores.begin(), cores.end(), c.core) == cores.end())
cores.push_back(c.core);
if (c.speed * 100 >= high->speed * 95 && std::find(fastCores.begin(), fastCores.end(), c.core) == fastCores.end())
fastCores.push_back(c.core);
}
// A real split into fast and slow cores (the Steam Machine's slow
// ones top out at 73%) needs at least two fast physical cores, so the
// game's own threads keep one. Cores alike (a desktop Ryzen's
// preferred-core ranking differs by a few percent, the Deck's not at
// all) need at least four, so the other threads keep three: the
// whole game uses up to 2.3 cores in the fly-in, the command
// processor one of them. On the Deck its busy time fell 2-6%, with
// fewer late frames. The reserved core is the fast one
// with the most capacity (ARM: the kernel's cpu_capacity, which
// weighs a core's size too; the Steam Frame's Cortex-X4, cpu 7, tops
// out at 3.05 GHz to its A720s' 3.15 but does 20% more), then the
// last (cpu 0's core stays shared; x86 has no cpu_capacity).
bool split = low->speed * 100 <= high->speed * 85;
if (split ? cpus.size() < 6 || fastCores.size() < 2 : fastOnly || cores.size() < 4)
return;
uint64_t core = 0, capacity = 0;
for (const Cpu& c : cpus)
if (std::find(fastCores.begin(), fastCores.end(), c.core) != fastCores.end() &&
(c.capacity > capacity || (c.capacity == capacity && c.core > core)))
{
core = c.core;
capacity = c.capacity;
}
CPU_ZERO(&s_reserved);
s_shared = allowed;
for (const Cpu& c : cpus)
if (c.core == core)
{
CPU_SET(c.cpu, &s_reserved);
CPU_CLR(c.cpu, &s_shared);
}
if (sched_setaffinity(0, sizeof(s_shared), &s_shared) != 0)
return;
s_reserving = true;
fprintf(stderr, "[cpu] command processor: cpus %s (a %score of its own); other threads: cpus %s\n",
CpuList(s_reserved).c_str(), split ? "fast " : "", CpuList(s_shared).c_str());
#endif
}
void UseReservedCore(const char* who)
{
#ifdef __linux__
if (s_reserving && sched_setaffinity(0, sizeof(s_reserved), &s_reserved) != 0)
fprintf(stderr, "[cpu] %s: could not move to its reserved core\n", who);
#else
(void)who;
#endif
}
void LeaveReservedCore()
{
#ifdef __linux__
if (s_reserving)
sched_setaffinity(0, sizeof(s_shared), &s_shared);
#endif
}
void TightenTimerSlack()
{
#ifdef __linux__
thread_local bool done = false;
if (!done)
{
prctl(PR_SET_TIMERSLACK, 1000UL, 0, 0, 0);
done = true;
}
#endif
}
}
+30
View File
@@ -0,0 +1,30 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Host CPU placement and waiting for threads that ran on a hardware thread of
// their own on the 360: the command processor, and guest code that spins
// while it waits for it.
#pragma once
namespace hostcpu
{
// Linux, first thing in main(): keeps one physical core, with its SMT
// sibling, for the command processor: one of the fastest when the host's
// cores differ in top speed (the Steam Machine's CPU has two 4.8 GHz
// cores and four 3.5 GHz ones), the last one when they are alike and
// there are at least four (the Deck: cpus 6,7). Every other thread,
// created by this one from now on, runs on the remaining CPUs. Nothing
// changes on 2-3 alike cores, or elsewhere. NFSMW_CP_OWN_CORE=0 turns it
// off, =fast keeps it to CPUs with fast and slow cores.
void ReserveFastCore();
// The command processor moves itself onto the reserved core.
void UseReservedCore(const char* who);
// For threads the command processor starts (they inherit its CPUs): back
// to the shared ones.
void LeaveReservedCore();
// Linux: a 1 us timer slack for the calling thread (the default 50 us
// turns a 50 us sleep into ~100 us). Cheap to call again.
void TightenTimerSlack();
}
+18
View File
@@ -0,0 +1,18 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
#include <stdafx.h>
#include "indirect.h"
void PPCCallIndirect(PPCContext& ctx, uint8_t* base, uint32_t target)
{
PPCFunc* fn = nullptr;
if (target >= PPC_CODE_BASE && target < PPC_CODE_BASE + PPC_CODE_SIZE && (target & 3) == 0)
fn = PPC_LOOKUP_FUNC(base, target);
if (fn == nullptr)
{
fprintf(stderr, "[cpu] indirect call to %08X, which is not a recompiled function (lr=%08X r3=%08X r4=%08X)\n",
target, uint32_t(ctx.lr), ctx.r3.u32, ctx.r4.u32);
fflush(stderr);
std::abort();
}
fn(ctx, base);
}
+12
View File
@@ -0,0 +1,12 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Force-included into the recompiled code: indirect calls (bctrl, bctr to a
// function pointer) go through a checked lookup that reports a bad target as
// a guest address instead of jumping to a wild host pointer.
#pragma once
#include <cstdint>
struct PPCContext;
void PPCCallIndirect(PPCContext& ctx, uint8_t* base, uint32_t target);
#define PPC_CALL_INDIRECT_FUNC(x) PPCCallIndirect(ctx, base, (x))
+15
View File
@@ -0,0 +1,15 @@
// Adapted from Unleashed Recompiled (https://github.com/hedge-dev/UnleashedRecomp),
// GPL-3.0-or-later. Modified for SpeedBreaker.
#pragma once
inline thread_local PPCContext* g_ppcContext;
inline PPCContext* GetPPCContext()
{
return g_ppcContext;
}
inline void SetPPCContext(PPCContext& ctx)
{
g_ppcContext = &ctx;
}
+81
View File
@@ -0,0 +1,81 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The game's Direct3D GPU waits. sub_825A5B20 is D3D's GPU hang check,
// called once per poll by each of its eight busy-wait loops (fence, ring
// space, idle: 0x82596364, 0x82596908, 0x82597208, 0x82597534, 0x825A995C,
// 0x825AA0E8, 0x825AAF08, 0x825AB234). It is overridden here to back those
// waits off to short sleeps, to park them while the game is suspended, and
// to report, with NFSMW_LOG_D3D_WAIT=1, which counter they poll and how often.
#include <stdafx.h>
#include <kernel/function.h>
#include <cpu/guest_time.h>
#include <cpu/host_cpu.h>
#include "timeline.h"
extern "C" PPC_FUNC(__imp__sub_825A5B20);
namespace
{
// On the 360 a spinning hardware thread cost nothing. Here the game's
// main thread spun a whole core at 100% while it waited for the command
// processor, and on the Steam Machine's CPU (two fast cores, four slow
// ones: see hostcpu::ReserveFastCore) the scheduler kept the spinning
// thread on a fast core. A wait still spins for its first 20 us, so short
// waits end on time; after that each poll sleeps 50 us. Linux only:
// macOS coalesces default-QoS timers to ~10 ms (see the command
// processor's Worker). NFSMW_GPU_WAIT_BACKOFF=0 spins throughout.
void BackOff()
{
#ifdef __linux__
static const bool backOff = [] { const char* v = std::getenv("NFSMW_GPU_WAIT_BACKOFF"); return !v || v[0] != '0'; }();
if (!backOff)
return;
using Clock = std::chrono::steady_clock;
thread_local Clock::time_point lastPoll{}, waitStart{};
auto now = Clock::now();
if (now - lastPoll > std::chrono::microseconds(200))
waitStart = now; // a new wait (one wait's polls are at most ~60 us apart)
lastPoll = now;
if (now - waitStart < std::chrono::microseconds(20))
return;
hostcpu::TightenTimerSlack();
std::this_thread::sleep_for(std::chrono::microseconds(50));
lastPoll = Clock::now();
#endif
}
}
PPC_FUNC(sub_825A5B20)
{
// While the game is suspended the GPU work these loops wait for is held
// back (Metal refuses it), and no guest time passes for the hang check:
// sleep through it instead of spinning a core (Apple has no back-off).
guesttime::WaitWhileSuspended();
if (timeline::g_enabled && !timeline::t_waitTargetMarked)
{
timeline::t_waitTargetMarked = true;
uint32_t device = PPC_LOAD_U32(ctx.r3.u32 + 0);
timeline::Mark(timeline::GameWaitTarget, PPC_LOAD_U32(device + 10396) - PPC_LOAD_U32(ctx.r3.u32 + 8));
}
static const bool log = std::getenv("NFSMW_LOG_D3D_WAIT") != nullptr;
if (log)
{
static uint64_t polls = 0;
static auto last = std::chrono::steady_clock::now();
polls++;
auto now = std::chrono::steady_clock::now();
if (now - last > std::chrono::seconds(1))
{
uint32_t args = ctx.r3.u32;
uint32_t device = PPC_LOAD_U32(args + 0);
uint32_t counterPtr = PPC_LOAD_U32(device + 10384);
fprintf(stderr, "[d3dwait] %llu polls/s | counter at %08X = %08X, issued %08X, target %08X\n",
(unsigned long long)polls, counterPtr, PPC_LOAD_U32(counterPtr), PPC_LOAD_U32(device + 10396),
PPC_LOAD_U32(args + 8));
polls = 0;
last = now;
}
}
BackOff();
__imp__sub_825A5B20(ctx, base);
}
+121
View File
@@ -0,0 +1,121 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See timeline.h.
#include <stdafx.h>
#include "timeline.h"
#include <cpu/guest_time.h>
#include <kernel/function.h>
namespace timeline
{
namespace
{
struct Event { int64_t ns; uint32_t arg; uint8_t kind; uint8_t thread; };
constexpr uint32_t kMax = 1 << 20;
constexpr int64_t kWindowNs = 200'000'000;
const char* kNames[Count] = { "wait+", "wait-", "present+", "present-", "batch+", "batch-",
"submit", "complete", "write/ev", "write/done", "swap", "regmem+", "regmem-", "interrupt", "submit+", "waittarget" };
std::chrono::steady_clock::time_point s_launch = std::chrono::steady_clock::now();
int64_t s_fromNs = [] {
const char* v = std::getenv("NFSMW_TIMELINE");
return v ? int64_t(std::atof(v) * 1e9) : int64_t(-1);
}();
std::unique_ptr<Event[]> s_events;
std::atomic<uint32_t> s_next{ 0 };
std::atomic<bool> s_dumped{ false };
std::atomic<uint8_t> s_threadIds{ 0 };
void Dump()
{
uint32_t n = std::min(s_next.load(), kMax);
std::vector<Event> ev(s_events.get(), s_events.get() + n);
std::sort(ev.begin(), ev.end(), [](auto& a, auto& b) { return a.ns < b.ns; });
FILE* f = fopen("build/timeline.txt", "w");
if (!f)
return;
// Summary: game wait share, GPU queue occupancy, per-kind counts.
int64_t t0 = ev.empty() ? 0 : ev.front().ns;
int64_t waitNs = 0, waitStart = -1, busyNs = 0, busyStart = -1;
int inFlight = 0;
uint32_t counts[Count] = {};
for (auto& e : ev)
{
counts[e.kind]++;
if (e.kind == GameWaitBegin && waitStart < 0) waitStart = e.ns;
if (e.kind == GameWaitEnd && waitStart >= 0) { waitNs += e.ns - waitStart; waitStart = -1; }
if (e.kind == Submit && inFlight++ == 0) busyStart = e.ns;
if (e.kind == Complete && inFlight > 0 && --inFlight == 0) busyNs += e.ns - busyStart;
}
double span = ev.empty() ? 1 : double(ev.back().ns - t0);
fprintf(f, "# %.1f ms: game waiting %.0f%%, GPU queue non-empty %.0f%%\n", span / 1e6, 100.0 * waitNs / span, 100.0 * busyNs / span);
for (int k = 0; k < Count; k++)
fprintf(f, "# %-10s %u\n", kNames[k], counts[k]);
for (auto& e : ev)
fprintf(f, "%10.3f t%u %-10s %08X\n", (e.ns - t0) / 1e6, e.thread, kNames[e.kind], e.arg);
fclose(f);
fprintf(stderr, "[timeline] wrote build/timeline.txt (%u events)\n", n);
}
}
bool g_enabled = s_fromNs >= 0;
void Record(Kind kind, uint32_t arg)
{
int64_t ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - s_launch).count();
if (ns < s_fromNs)
return;
if (ns > s_fromNs + kWindowNs)
{
if (!s_dumped.exchange(true))
{
g_enabled = false;
Dump();
}
return;
}
static std::once_flag once;
std::call_once(once, [] { s_events.reset(new Event[kMax]); });
thread_local uint8_t thread = s_threadIds.fetch_add(1);
uint32_t i = s_next.fetch_add(1);
if (i < kMax)
s_events[i] = { ns, arg, uint8_t(kind), thread };
}
}
// The game's D3D wait for GPU progress, and its Present. Timed in guest
// time (the host's on desktop), which stands still while the game is
// suspended: a wait or a frame across a suspension counts only the time the
// game ran, as the command processor's frame times do.
namespace
{
int64_t NowNs()
{
return guesttime::NowNs();
}
}
extern "C" PPC_FUNC(__imp__sub_82597498);
PPC_FUNC(sub_82597498)
{
timeline::Mark(timeline::GameWaitBegin, ctx.r4.u32);
timeline::t_waitTargetMarked = false;
int64_t start = NowNs();
__imp__sub_82597498(ctx, base);
timeline::g_gameWaitNs.fetch_add(uint64_t(NowNs() - start), std::memory_order_relaxed);
timeline::Mark(timeline::GameWaitEnd);
}
extern "C" PPC_FUNC(__imp__sub_82593A58);
PPC_FUNC(sub_82593A58)
{
timeline::Mark(timeline::GamePresentBegin);
static std::atomic<int64_t> lastPresent{ 0 };
int64_t start = NowNs();
if (int64_t previous = lastPresent.exchange(start))
{
uint64_t frame = uint64_t(start - previous), longest = timeline::g_gameLongestFrameNs.load(std::memory_order_relaxed);
while (frame > longest && !timeline::g_gameLongestFrameNs.compare_exchange_weak(longest, frame)) {}
}
__imp__sub_82593A58(ctx, base);
timeline::g_gamePresentNs.fetch_add(uint64_t(NowNs() - start), std::memory_order_relaxed);
timeline::Mark(timeline::GamePresentEnd);
}
+40
View File
@@ -0,0 +1,40 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// NFSMW_TIMELINE=<seconds>: from that many seconds after launch, record
// 200 ms of pipeline events (game waits, command processor batches,
// submissions, completions, fence publication) and write them to
// build/timeline.txt with a per-event summary. Near-zero cost when off.
#pragma once
#include <atomic>
#include <cstdint>
namespace timeline
{
enum Kind : uint8_t
{
GameWaitBegin, GameWaitEnd, // D3D wait for GPU progress (arg: target)
GamePresentBegin, GamePresentEnd,
BatchBegin, BatchEnd, // command processor ring batch (arg: dwords)
Submit, // arg: slot | writes << 8
Complete, // arg: slot
WriteByEvent, WriteAtComplete, // deferred write published
Swap, // XE_SWAP processed
RegMemBegin, RegMemEnd, // a blocking WAIT_REG_MEM (arg: address/register)
Interrupt, // graphics interrupt delivered (arg: source)
SubmitBegin, // SubmitRecorded entered (arg: site)
GameWaitTarget, // first poll of a D3D wait (arg: issued - target fence)
Count
};
extern bool g_enabled;
inline thread_local bool t_waitTargetMarked = true;
// Always counted, for the command processor's hitch reports: time the
// game spent in D3D's GPU waits and in Present, and its longest frame
// (Present to Present) since the report last took it.
inline std::atomic<uint64_t> g_gameWaitNs{ 0 }, g_gamePresentNs{ 0 }, g_gameLongestFrameNs{ 0 };
void Record(Kind kind, uint32_t arg = 0);
inline void Mark(Kind kind, uint32_t arg = 0)
{
if (g_enabled) [[unlikely]]
Record(kind, arg);
}
}
+148
View File
@@ -0,0 +1,148 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See frame_time.h.
//
// NFSMW times itself with mftb. The main loop (sub_823B0008) measures each
// frame and hands it, in milliseconds as 16.16 fixed point, to the frame
// (sub_823AFE20). That quantises it to 1/3600 s (sub_823C6500; the frame's
// time for its real-time users, [0x82A39660]), advances RealTimeFrames and a
// 4000 Hz tick counter at 0x82A399AC (AdvanceRealTime, 0x823C66C8), and runs
// the world simulation in fixed 1/60 s steps from an accumulator over those
// ticks (sub_823A2348: floor(elapsed / step) steps, the remainder kept; the
// step is stored once, at boot, and tasks copy it). There is no interpolation:
// a frame's world state is its steps'.
//
// So a frame that isn't 1/60 s long shows it. With the vblank locked to the
// player's 59.9726 Hz monitor, a frame lasts 16.674 ms and every 36.6 s one
// runs two steps: the car jumps, where the presenter used to drop a frame.
// And at any rate a frame's measured length jitters (the main thread's
// wake-up, the 250 us ticks), so while the accumulator's remainder sits near
// a step boundary, frames alternate between no step and two.
//
// Each guest vblank is made one step instead (NFSMW_GAME_TIME=0 turns this
// off). A frame is given k/60 s for the k guest vblanks that fired during it
// (counted from one frame's start to the next, 1 to 6), as the console's
// 60 Hz vblank would have given it; the stepper's elapsed time, then k steps
// give or take a tick, is rounded to k. The main loop starts a frame when D3D
// lets it, after the vblank that released the last one, so its start can
// jitter by most of a vblank without changing the count; a frame slower than
// the vblank starts anywhere between two, and its count is its length
// rounded either way. A frame no vblank fired in keeps its measured time,
// which later frames give back (game/whole_vblanks.h): game time adds up to
// the vblanks that fired. It runs at the display's rate over 60: 0.044% slow
// on a 59.97 Hz monitor, at most 2.5% (the lock's range). Frames of more
// than 6 vblanks (loading) keep their measured time. At Frame Rate 30
// (video::GuestVblankDivider) a guest vblank comes every other refresh and
// is given 2/60 s: two steps a frame, the game at its speed.
#include <stdafx.h>
#include "frame_time.h"
#include "whole_vblanks.h"
#include <kernel/function.h>
#include <video/presenter.h>
#include <video/vblank_lock.h>
namespace
{
const bool s_enabled = [] {
const char* v = std::getenv("NFSMW_GAME_TIME");
bool on = !v || v[0] != '0';
if (!on)
fprintf(stderr, "[game] game time as measured (NFSMW_GAME_TIME=0)\n");
return on;
}();
std::atomic<uint64_t> s_frames{ 0 }, s_snapped{ 0 }, s_noStep{ 0 }, s_multiStep{ 0 };
// NFSMW_GAME_TIME_LOG=<file>: one line per frame, written when the next
// one starts: its measured ms, guest vblanks, whether it was given whole
// vblanks, the world steps it ran (-1: no world), how long after the last
// vblank it started (us), and the guest vblanks it was given (0: its
// measured time).
FILE* const s_log = [] {
const char* path = std::getenv("NFSMW_GAME_TIME_LOG");
FILE* f = path ? fopen(path, "w") : nullptr;
if (f)
fprintf(f, "measured_ms,vblanks,snapped,steps,after_vblank_us,given\n");
return f;
}();
struct LogLine
{
double measuredMs = 0, afterVblankUs = 0;
uint64_t vblanks = 0, given = 0;
int steps = -1;
bool snapped = false, pending = false;
} s_line; // main thread only
game::WholeVblanks s_whole; // main thread only
// Whole vblanks are game time only at the lock's rates: NFSMW_VBLANK_HZ
// far from 60 keeps game time as measured, in the stepper too.
bool NearSixty(double vblankMs)
{
return vblankMs >= 1000.0 / video::RefreshFit::kMaxHz && vblankMs <= 1000.0 / video::RefreshFit::kMinHz;
}
}
namespace game
{
FrameTimeStats GetFrameTimeStats()
{
return { s_frames.load(std::memory_order_relaxed), s_noStep.load(std::memory_order_relaxed),
s_multiStep.load(std::memory_order_relaxed), s_snapped.load(std::memory_order_relaxed) };
}
}
// GameFrameTime, before `bl 0x823AFE20` at 0x823B0148 in the main loop: r3 is
// the frame's measured time, milliseconds in 16.16 fixed point.
void GameFrameTime(PPCRegister& r3)
{
static uint64_t last = 0;
uint64_t now = video::GuestVblanks(), vblanks = now - last;
last = now;
s_frames.fetch_add(1, std::memory_order_relaxed);
double measuredMs = double(int32_t(r3.u32)) / 65536.0;
if (s_log)
{
if (s_line.pending)
fprintf(s_log, "%.3f,%llu,%d,%d,%.0f,%llu\n", s_line.measuredMs, (unsigned long long)s_line.vblanks, s_line.snapped,
s_line.steps, s_line.afterVblankUs, (unsigned long long)s_line.given);
int64_t since = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() -
video::LastGuestVblank();
s_line = { measuredMs, double(since) / 1e3, vblanks, 0, -1, false, true };
}
if (!s_enabled)
return;
double refreshMs = video::GuestVblank().PeriodNs() / 1e6;
if (!NearSixty(refreshMs))
{
s_whole.Reset();
return;
}
// At Frame Rate 30 a guest vblank is two refreshes, and two 1/60 s steps.
int divider = video::GuestVblankDivider();
uint64_t given = s_whole.Give(measuredMs, vblanks, refreshMs * divider);
if (!given)
return;
r3.u64 = uint32_t(std::llround(double(given * divider) * (1000.0 / 60.0) * 65536.0));
s_snapped.fetch_add(1, std::memory_order_relaxed);
s_line.snapped = true;
s_line.given = given;
}
// WorldStepTime, before `fadds f0,f10,f9` at 0x823A23E8 in the stepper: f10 is
// the elapsed time in steps (f9 the remainder it joins).
void WorldStepTime(PPCRegister& f10)
{
if (!s_enabled || !NearSixty(video::GuestVblank().PeriodNs() / 1e6))
return;
double k = std::round(f10.f64);
if (k >= 1 && k <= 6 && std::abs(f10.f64 - k) <= 0.05)
f10.f64 = k;
}
// WorldSteps, at `mr r4,r30` (0x823A2424) in the stepper: r30 is the number of
// steps this frame runs, before the step runner's cap.
void WorldSteps(PPCRegister& r30)
{
s_line.steps = int(r30.u32);
if (r30.u32 == 0)
s_noStep.fetch_add(1, std::memory_order_relaxed);
else if (r30.u32 >= 2)
s_multiStep.fetch_add(1, std::memory_order_relaxed);
}
+18
View File
@@ -0,0 +1,18 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Game time in whole vblanks (frame_time.cpp): each guest vblank is one world
// step, at any display rate the vblank locks to.
#pragma once
#include <cstdint>
namespace game
{
// Since launch, for the [perf] line: game frames (main loop iterations),
// those whose world ran no step or two and more, and those given whole
// vblanks of time.
struct FrameTimeStats
{
uint64_t frames, noStep, multiStep, snapped;
};
FrameTimeStats GetFrameTimeStats();
}
+165
View File
@@ -0,0 +1,165 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Movies on whole pairs of vblanks.
//
// The game's movies (29.97 fps WMVs) play through the XDK's XMV player. Its
// render call (sub_826E8CF8, the player's vtable slot 18, from the game's
// movie update sub_8245DD20) works out how many whole milliseconds are left
// until the next frame is due, Sleeps that long and renders the frame; the
// game then swaps with presentation interval ONE. D3D's swap sync
// (sub_82598670) has the command processor raise an interrupt whose
// callback (sub_825985A0, argument 0x0001000A: interval 1, immediate
// threshold 10%) sets how many vblank interrupts to wait, and the CP's
// WAIT_REG_MEM on 0x06449006 lets the swap through when the vblank
// interrupt (sub_82597768) counts that down: the frame goes out at the first
// vblank after the CP reaches the swap. The player's clock (sub_826DCA50) is
// the performance counter in whole ms against a base that, by the
// disassembly, its audio side moves to "now minus the audio played"
// (sub_826DCCF0), and the audio moves 256 samples (5.33 ms) at a time. So
// the kicks scatter around their ideal times: over 4 s of the attract movie
// (Steam Machine, nested gamescope at 60 Hz) they sat -5.6..+5.8 ms (sd 2.4
// ms) from a straight line, with 5-8 ms steps. The sleeps wake on time (10 us
// late) and the WAIT_REG_MEM ends 0.05-0.15 ms after the vblank interrupt.
// While the kicks' phase is within that scatter of a vblank, frames fall on
// either side of it and show for 1 and 3 vblanks instead of 2 and 2: 7-10% of
// movie frames (the "WAIT_REG_MEM" late frames of the player's hitch reports).
//
// At 60 Hz, or the rate the vblank locks to (the Deck's panel: 60.16 Hz),
// a 29.97 fps movie's phase crosses a vblank every 4.6-17 s. So a movie
// frame that would go out one vblank after the last one waits for the next
// vblank when its swap came less than the guard (10 ms) before that vblank:
// a kick that early is the clock's scatter, not a movie running ahead. One
// that comes earlier still (a clock faster than two vblanks a frame) goes out
// at once: that is where such a movie needs its 1. A late frame can't be
// helped, so a 3 stays a 3, but the frames after it keep the later phase,
// clear of the next vblank: a slow clock gets one 3 per crossing and no 1.
// A held frame shows one vblank later than it would have
// (tests/movie_cadence_sim.py replays the measured scatter).
//
// Only frames the XMV player rendered are held, after four movie frames of
// two vblanks or more in a row, at guest vblank rates near 60 Hz (RefreshFit's
// range, which takes in the free-running 16.667 ms under a display the lock
// rejects, 120 Hz say): the game's own frames, and anything that runs at a
// vblank a frame, are never touched.
// NFSMW_MOVIE_CADENCE=0 turns it off, NFSMW_MOVIE_CADENCE=<ms> sets the
// guard (default 10, at most 12). Each movie leaves a [movie] line: its
// frames by vblanks shown, and those held.
#include <stdafx.h>
#include <cpu/guest_time.h>
#include <kernel/function.h>
#include <video/presenter.h>
#include <video/refresh_fit.h>
#include <video/vblank_lock.h>
extern "C" PPC_FUNC(__imp__sub_826E8CF8); // XMV player: sleep until the next frame is due, render it
extern "C" PPC_FUNC(__imp__sub_825985A0); // D3D: the swap's interrupt callback
namespace
{
int64_t NowNs()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
const double s_guardMs = [] {
const char* v = std::getenv("NFSMW_MOVIE_CADENCE");
double ms = v ? std::atof(v) : 10.0;
if (v && ms <= 0)
fprintf(stderr, "[movie] cadence off (NFSMW_MOVIE_CADENCE=0): movie frames go out at the first vblank after their swap\n");
return std::clamp(ms, 0.0, 12.0);
}();
constexpr uint32_t kSteadyFrames = 4; // movie frames of 2+ vblanks before any hold
// D3D's device: vblank count, the vblank that released the last swap,
// and the vblanks left before the pending swap goes (sub_825985A0 and
// the vblank interrupt, sub_82597768, keep them).
constexpr uint32_t kDeviceVblanks = 15600, kDeviceReleased = 15604, kDeviceCountdown = 15608;
std::atomic<uint64_t> s_movieRenders{ 0 }; // XMV frames rendered
// The swap callback's thread only: the command processor's (with
// NFSMW_EAGER_WRITES=0 also the renderer's completion thread, one swap at
// a time).
struct
{
uint64_t renders = 0; // s_movieRenders at the previous swap
bool lastWasMovie = false; // the previous swap was a movie frame
uint32_t lastReleased = 0; // the release vblank before the previous swap's
uint32_t steady = 0; // consecutive movie frames of 2+ vblanks
uint64_t frames = 0, spans[5] = {}, held = 0;
} s_state;
void EndMovie()
{
auto& s = s_state;
if (s.frames)
fprintf(stderr, "[movie] %llu frames: %llu on 1 vblank, %llu on 2, %llu on 3, %llu on 4+; %llu held a vblank (%.1f ms guard%s)\n",
(unsigned long long)s.frames, (unsigned long long)s.spans[1], (unsigned long long)s.spans[2],
(unsigned long long)s.spans[3], (unsigned long long)s.spans[4], (unsigned long long)s.held, s_guardMs,
s_guardMs > 0 ? "" : ": off");
s.frames = s.held = 0;
std::fill(std::begin(s.spans), std::end(s.spans), 0);
s.steady = 0;
}
}
PPC_FUNC(sub_826E8CF8)
{
__imp__sub_826E8CF8(ctx, base);
s_movieRenders.fetch_add(1, std::memory_order_release);
}
PPC_FUNC(sub_825985A0)
{
uint32_t arg = ctx.r3.u32;
__imp__sub_825985A0(ctx, base);
auto& s = s_state;
int64_t now = NowNs();
uint32_t device = PPC_LOAD_U32(PPC_LOAD_U32(0x82000B48)); // (KeGetCurrentProcessType() is 1: the title's)
uint32_t released = PPC_LOAD_U32(device + kDeviceReleased);
// A movie frame: the XMV player rendered since the last swap (the game
// thread waits for that swap before it renders the next frame).
uint64_t renders = s_movieRenders.load(std::memory_order_acquire);
bool movie = renders != s.renders;
s.renders = renders;
// The previous swap went out at `released`: its vblanks on screen.
if (s.lastWasMovie)
{
uint32_t span = released - s.lastReleased;
s.frames++;
s.spans[std::min<uint32_t>(span, 4)]++;
s.steady = span >= 2 ? s.steady + 1 : 0;
}
if (!movie && s.lastWasMovie)
EndMovie();
s.lastReleased = released;
s.lastWasMovie = movie;
if (!movie || s_guardMs <= 0 || arg >> 16 != 1 || s.steady < kSteadyFrames)
return;
// Due one vblank after the last release (no vblank since it, one to
// wait): within the guard of that vblank, wait for the one after.
if (PPC_LOAD_U32(device + kDeviceVblanks) != released)
return;
double period = video::GuestVblank().PeriodNs();
if (period < 1e9 / video::RefreshFit::kMaxHz || period > 1e9 / video::RefreshFit::kMinHz)
return;
// Suspended (iOS: the app isn't active), or no vblank since the resume:
// the time since the last vblank (host clock, like LastResumeNs) spans
// the suspension, so the hold would keep the frame back for nothing.
if (guesttime::Suspended() || video::LastGuestVblank() < guesttime::LastResumeNs())
return;
double sinceVblank = double(now - video::LastGuestVblank());
if (sinceVblank < period - s_guardMs * 1e6)
return;
// The vblank interrupt may have run since the callback set 1; hold only
// a countdown still at 1 (0 means it already let this swap go).
std::atomic_ref countdown(*reinterpret_cast<uint32_t*>(base + device + kDeviceCountdown));
uint32_t expected = ByteSwap(1u);
if (countdown.compare_exchange_strong(expected, ByteSwap(2u)))
s.held++;
}
+25
View File
@@ -0,0 +1,25 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// NFSMW's deferred render-callback queue (state at 0x82909650) is a
// lock-free single-producer, single-consumer list: while loading and during
// in-engine cutscenes one thread records render callbacks (sub_823C8210, and
// the same code inlined in sub_82444520 and sub_82462DC8) and the main
// thread plays them back (sub_823C8290). It has no barriers: it relies on the
// entry's stores becoming visible before the write pointer and count that
// publish it. x86 keeps stores in order; ARM64 doesn't, and the main thread
// saw a new count with the entry still zero (a call to a null callback at
// lr 823C82F8, about one run in three on the Mac once game time ran at the
// right speed). Mid-asm hooks (config/nfsmw.toml) add a release fence before
// each publish and an acquire fence before an entry is read.
#include <stdafx.h>
#include <atomic>
void RenderQueuePublish()
{
std::atomic_thread_fence(std::memory_order_release);
}
void RenderQueueAcquire()
{
std::atomic_thread_fence(std::memory_order_acquire);
}
+233
View File
@@ -0,0 +1,233 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See ultrawide.h.
//
// Three mid-asm hooks (config/nfsmw.toml):
// - UltrawideProjection, before `stfs f12,0(r30)` at 0x8243EE18 in the view
// setup (sub_8243EAC0, CreateViewMatricies in the PC symbols): f12 is the
// projection's x scale, P[0][0] = 1 / tan(half the horizontal FOV),
// stored once and not used again as a register; f10 is 1 / tan(half the
// vertical FOV), which the next instructions negate (`fneg f9,f10`) and
// store as P[1][1] (`stfs f9,84(r31)`; the game's y is flipped) and
// which is overwritten before it is read again. Narrowing f10's magnitude
// by scaleY widens the vertical FOV and leaves the horizontal one. The
// view-projection and the frustum planes are built from the stored
// matrix afterwards, so culling widens with the view (the top and bottom
// planes from its column 1); the LOD distance scale (sub_8243EA08, from
// the horizontal FOV only) stays. r28 is the view (the function's second
// argument); its id is at +4. Only the views the game itself widens for
// 16:9 are changed: 1, 2, 4, 5 and 17 (the player's camera, the road
// reflection and the shadow frustum among them), so the shadow maps
// (13-16) and the env cube (18-23) keep theirs.
// - UltrawideFrontEnd, at the branch at 0x82440E4C that ends the
// widescreen case of the front-end matrix (sub_82440D80): f13 is its x
// scale (0.92 safe area x 0.75, a 4:3 canvas centred in 16:9) and f0 its
// y scale (0.92), stored at m00 and m11 (shader constants c0 and c5).
// - UltrawideMirror, at the branch at 0x8245CED8 that ends the widescreen
// case of the rear-view mirror draw (sub_8245CDF0). The mirror is not a
// view: its picture is one quad whose pixel shader (ps_40d7f669a326708d)
// samples the car's environment cube map (views 18-23) along per-corner
// directions fixed in the vertex data, drawn through a pass-through
// vertex shader (vs_58da94362438089c) in clip coordinates the CPU builds
// (sub_8245CA98: x = 2 * f1 - 1, width = 2 * f3; 0.638 and -0.275, so x
// runs from +0.276 to -0.274, mirrored; y = 2 * f2 - 1, height = 2 * f4).
// The front-end matrix never touches it, so without this the picture
// stays 16:9-wide while its frame (a HUD sprite) narrows, and the stretch
// puts it 1.34x past the frame. Narrowed about the screen centre like the
// HUD; the directions stay, so the picture keeps its proportions after
// the stretch.
//
// A ppc/ generated before Vert+ calls the hooks with the x registers only
// (the overloads below): Hor+ keeps working and TallHooksActive() stays
// false, so the presenter letterboxes a narrower screen as before instead
// of the build failing on an undefined symbol.
#include <stdafx.h>
#include "ultrawide.h"
#include <kernel/function.h>
namespace
{
// Movies (the XDK's XMV player; every file in Movies/ is 16:9 WMV3) are
// fitted by the player itself. At each movie's start its renderer setup
// (sub_826D8078) loads the display aspect, 16/9, from this .rdata float
// (`lfs f0` at 0x826D8114, its only reader) and keeps display aspect /
// backbuffer aspect at renderer+140; the auto-fit (sub_826D8640, run from
// the format callback sub_826DB140 before the first frame) pillarboxes a
// movie narrower than that and letterboxes one wider (loc_826D86C8: the
// full width, centred rows). Given the aspect the 1280x720 frame is shown
// at, it draws the movie into the centre 1280 * scaleX columns or 720 *
// scaleY rows, as the HUD is, so after the presenter's stretch every frame
// of it is 16:9 between black bars, with no per-frame decision.
constexpr uint32_t kMovieDisplayAspect = 0x8200FAC0;
// Both scales in one atomic, so a hook never sees one from before a change
// and the other from after it.
struct Scales
{
float x, y;
};
std::atomic<Scales> s_scales{ Scales{ 1.0f, 1.0f } };
std::atomic<bool> s_hooksSeen{ false };
std::atomic<bool> s_tallHooksSeen{ false };
std::atomic<float> s_frontEndX{ 0.0f };
std::atomic<float> s_frontEndY{ 0.0f };
// Log each view id once, with its target size (the per-view table at
// 0x82C3F590 + id * 384: target pointer at +368, width/height at +16/+20).
void LogView(uint32_t id, bool widened)
{
static std::atomic<uint64_t> seen{ 0 };
if (id >= 64 || (seen.fetch_or(1ull << id) & (1ull << id)))
return;
uint32_t target = ByteSwap(*static_cast<const uint32_t*>(g_memory.Translate(0x82C3F590 + id * 384 + 368)));
uint32_t w = 0, h = 0;
if (target)
{
w = ByteSwap(*static_cast<const uint32_t*>(g_memory.Translate(target + 16)));
h = ByteSwap(*static_cast<const uint32_t*>(g_memory.Translate(target + 20)));
}
fprintf(stderr, "[uw] view %u: target %ux%u%s\n", id, w, h, widened ? ", widened" : "");
}
// The view at `view` (guest address) is one the game widens for 16:9.
bool WidenedView(uint32_t view)
{
uint32_t id = ByteSwap(*static_cast<const uint32_t*>(g_memory.Translate(view + 4)));
bool widen = id == 1 || id == 2 || id == 4 || id == 5 || id == 17;
LogView(id, widen);
return widen;
}
// `stfs` stores the register as a float.
void ScaleRegister(PPCRegister& f, float scale)
{
f.f64 = double(float(f.f64) * scale);
}
// About the screen centre (0.5 of it), for the mirror's 0..1 edge.
void ScaleAboutCentre(PPCRegister& f, float scale)
{
f.f64 = double(0.5f + (float(f.f64) - 0.5f) * scale);
}
}
namespace game
{
void SetAspectScale(float scaleX, float scaleY)
{
const Scales next{ std::clamp(scaleX, 0.1f, 1.0f), std::clamp(scaleY, 0.5f, 1.0f) };
const Scales last = s_scales.exchange(next, std::memory_order_relaxed);
if (last.x != next.x || last.y != next.y)
fprintf(stderr, "[uw] view scale x %.4f y %.4f\n", next.x, next.y);
// The movie player's display aspect, stored with every decision (one
// 4-byte store per presented frame) so nothing depends on when the
// image was loaded. Read once per movie, at its start: a change made
// while a movie plays applies from the next movie.
float aspect = (16.0f / 9.0f) * next.y / next.x;
uint32_t bits;
memcpy(&bits, &aspect, sizeof(bits));
*static_cast<uint32_t*>(g_memory.Translate(kMovieDisplayAspect)) = ByteSwap(bits);
}
float AspectScaleX()
{
return s_scales.load(std::memory_order_relaxed).x;
}
float AspectScaleY()
{
return s_scales.load(std::memory_order_relaxed).y;
}
bool HooksActive()
{
return s_hooksSeen.load(std::memory_order_relaxed);
}
bool TallHooksActive()
{
return s_tallHooksSeen.load(std::memory_order_relaxed);
}
float FrontEndScaleX()
{
return s_frontEndX.load(std::memory_order_relaxed);
}
float FrontEndScaleY()
{
return s_frontEndY.load(std::memory_order_relaxed);
}
}
void UltrawideProjection(PPCRegister& r28, PPCRegister& f12, PPCRegister& f10)
{
if (!s_tallHooksSeen.load(std::memory_order_relaxed))
{
s_hooksSeen.store(true, std::memory_order_relaxed);
s_tallHooksSeen.store(true, std::memory_order_relaxed);
}
const Scales scale = s_scales.load(std::memory_order_relaxed);
if (scale.x == 1.0f && scale.y == 1.0f)
return;
if (!WidenedView(r28.u32))
return;
if (scale.x != 1.0f)
ScaleRegister(f12, scale.x);
if (scale.y != 1.0f)
ScaleRegister(f10, scale.y); // its magnitude: negated into P[1][1] after this
}
void UltrawideFrontEnd(PPCRegister& f13, PPCRegister& f0)
{
const Scales scale = s_scales.load(std::memory_order_relaxed);
if (scale.x != 1.0f)
ScaleRegister(f13, scale.x);
if (scale.y != 1.0f)
ScaleRegister(f0, scale.y);
s_frontEndX.store(float(f13.f64), std::memory_order_relaxed);
s_frontEndY.store(float(f0.f64), std::memory_order_relaxed);
}
void UltrawideMirror(PPCRegister& f1, PPCRegister& f2, PPCRegister& f3, PPCRegister& f4)
{
const Scales scale = s_scales.load(std::memory_order_relaxed);
if (scale.x != 1.0f)
{
ScaleAboutCentre(f1, scale.x);
ScaleRegister(f3, scale.x);
}
if (scale.y != 1.0f)
{
ScaleAboutCentre(f2, scale.y);
ScaleRegister(f4, scale.y);
}
}
// The hooks as a ppc/ generated before Vert+ calls them: x only.
void UltrawideProjection(PPCRegister& r28, PPCRegister& f12)
{
if (!s_hooksSeen.load(std::memory_order_relaxed))
s_hooksSeen.store(true, std::memory_order_relaxed);
const float scale = s_scales.load(std::memory_order_relaxed).x;
if (scale == 1.0f)
return;
if (WidenedView(r28.u32))
ScaleRegister(f12, scale);
}
void UltrawideFrontEnd(PPCRegister& f13)
{
const float scale = s_scales.load(std::memory_order_relaxed).x;
if (scale != 1.0f)
ScaleRegister(f13, scale);
s_frontEndX.store(float(f13.f64), std::memory_order_relaxed);
}
void UltrawideMirror(PPCRegister& f1, PPCRegister& f3)
{
const float scale = s_scales.load(std::memory_order_relaxed).x;
if (scale == 1.0f)
return;
ScaleAboutCentre(f1, scale);
ScaleRegister(f3, scale);
}
+42
View File
@@ -0,0 +1,42 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Screens of another shape than 16:9 (Aspect Ratio on Auto, "Fill Screen";
// video/picture_fit decides): the game keeps rendering its 1280x720 frame,
// the presenter stretches it to fill the screen, and the view is changed so
// the stretch doesn't distort it.
// - Hor+, a screen wider than 16:9 (ultrawide): the projection's horizontal
// scale is narrowed by scaleX = (16/9) / screen aspect, so the view
// widens to the screen's aspect.
// - Vert+, a screen narrower than 16:9 (a 16:10 Steam Deck or Mac, a 4:3
// iPad): the projection's vertical scale is narrowed by scaleY = screen
// aspect / (16/9), so the view grows taller; the horizontal view stays.
// The HUD's matrix is narrowed by the same factor, so after the stretch it
// keeps its shape and size in a centred 16:9 block, and so is the rear-view
// mirror's picture. Movies are fitted by the movie player, told the screen's
// aspect: pillarboxed on a wider screen, letterboxed on a narrower one.
#pragma once
namespace game
{
// The view scales: (16/9) / screen aspect in x (Hor+; 0.744 at
// 3440x1440), screen aspect / (16/9) in y (Vert+; 0.75 on a 4:3 iPad, 0.9
// on a Steam Deck); 1 and 1 for 16:9 (no change). At most one is below 1.
// Any thread (the presenter sets them with each new frame; the game's
// render thread reads them). Guest memory must be set up (it also writes
// the movie player's display aspect).
void SetAspectScale(float scaleX, float scaleY);
float AspectScaleX();
float AspectScaleY();
// The hooks have run (the recompiled code has them): without them a
// stretched frame would just be distorted.
bool HooksActive();
// The hooks that also take the vertical scale have run. A ppc/ generated
// from a config/nfsmw.toml before Vert+ has the horizontal ones only: a
// screen narrower than 16:9 is then letterboxed as before.
bool TallHooksActive();
// The front-end (HUD and menu) matrix's x and y scales as
// UltrawideFrontEnd last left them (0 before the first front-end frame):
// the renderer recognises front-end draws by them.
float FrontEndScaleX();
float FrontEndScaleY();
}
+66
View File
@@ -0,0 +1,66 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// How GameFrameTime (frame_time.cpp) gives game frames whole guest vblanks
// of time, apart so tests/whole_vblanks_test.cpp can check it.
#pragma once
#include <algorithm>
#include <cmath>
#include <cstdint>
namespace game
{
// Game time follows the guest vblanks that fire: over any run of frames
// it adds up to them, to within a vblank, and each frame that saw any
// gets whole ones (1 to 6), so the fixed 1/60 s world step runs a whole
// number of times a frame.
// - A frame the vblank paces (60 fps held, or 30 at Frame Rate 30) saw
// one and gets one.
// - A frame it doesn't (GPU-bound) starts anywhere between two, so its
// count is its length rounded down or up: a 44 ms frame (2.64
// refreshes) sees 2 or 3, and gets them.
// - A frame no vblank fired in (a vblank thread waking late, as the
// Mac's does several ms at a time when its GPU is busy; or two frames
// between two vblanks) keeps its measured time, and is then ahead of
// the vblanks by that much: the next frames give it back, one whole
// vblank fewer when it adds up to one, never fewer than one.
// The rule until 2026-10-07 gave a frame its count only when its
// measured time was within half a vblank of it, else the measured time:
// GPU-bound 44 ms frames got 3 vblanks or their own 2.64, so the game ran
// fast (3-6% on an Intel laptop GPU), and at Frame Rate 30 (44-49 ms
// frames, 1 or 2 guest vblanks of 33.3 ms) 9-15% slow; a late vblank
// thread's 0 and 2 counts around 18 ms frames (the Mac at 3x3) ran it
// 6-9% slow.
struct WholeVblanks
{
// Game time given beyond the vblanks that fired, in vblanks: at most
// kMaxAhead (a held vblank, as a GPU-only suspension's, isn't
// replayed: what can't be given back is forgotten).
double ahead = 0.0;
static constexpr double kMaxAhead = 6.0;
// A frame the main loop measured at `measuredMs`, during which
// `vblanks` guest vblanks fired (from its start to the next frame's),
// each `vblankMs` long: the vblanks to give it, or 0 to keep its
// measured time. More than 6 (loading) keeps it too, and starts over.
uint64_t Give(double measuredMs, uint64_t vblanks, double vblankMs)
{
if (vblanks > 6 || !(vblankMs > 0.0))
{
ahead = 0.0;
return 0;
}
if (vblanks == 0)
{
ahead = std::min(ahead + std::max(measuredMs, 0.0) / vblankMs, kMaxAhead);
return 0;
}
double owed = double(vblanks) - ahead;
int64_t give = std::clamp<int64_t>(std::llround(owed), 1, 6);
ahead = double(give) - owed;
return uint64_t(give);
}
// Game time measured again (NFSMW_GAME_TIME=0, a rate far from 60).
void Reset() { ahead = 0.0; }
};
}
+119
View File
@@ -0,0 +1,119 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The free ranges of one block of device memory, for images bound at
// offsets into a few large allocations instead of an allocation each
// (gpu/renderer.cpp, image memory). Best fit over the free ranges, the
// range's start rounded up to the alignment asked for (the gap before it
// stays free); a freed range merges with free neighbours, so a block whose
// images are all gone is one free range again. Every range in a block is an
// optimally tiled image, so bufferImageGranularity never applies between
// neighbours. Vulkan-free: tests/block_ranges_test.cpp checks it alone.
#pragma once
#include <cstdint>
#include <iterator>
#include <map>
namespace gpu
{
class BlockRanges
{
public:
static constexpr uint64_t kNone = ~0ull;
explicit BlockRanges(uint64_t size) : m_size(size)
{
if (size)
m_free.emplace(0, size);
}
// The offset of `size` bytes at a multiple of `alignment` (a power of
// two; 0 counts as 1), or kNone when no free range holds them. Of the
// ranges that do, the one that leaves the least over (the lowest
// offset among equals).
uint64_t Allocate(uint64_t size, uint64_t alignment)
{
if (size == 0 || size > m_size - m_used)
return kNone;
if (alignment == 0)
alignment = 1;
auto best = m_free.end();
uint64_t bestStart = 0, bestLeft = ~0ull;
for (auto it = m_free.begin(); it != m_free.end(); ++it)
{
uint64_t end = it->first + it->second;
uint64_t start = (it->first + alignment - 1) & ~(alignment - 1);
if (start < it->first || start >= end || end - start < size)
continue;
uint64_t left = it->second - size;
if (left < bestLeft)
{
best = it;
bestStart = start;
bestLeft = left;
if (left == 0)
break;
}
}
if (best == m_free.end())
return kNone;
uint64_t offset = best->first, end = best->first + best->second;
m_free.erase(best);
if (bestStart > offset)
m_free.emplace(offset, bestStart - offset);
if (bestStart + size < end)
m_free.emplace(bestStart + size, end - (bestStart + size));
m_used += size;
m_ranges++;
return bestStart;
}
// Gives back a range Allocate returned. False (and nothing changes)
// when it isn't one: outside the block or over a free range.
bool Free(uint64_t offset, uint64_t size)
{
if (size == 0 || offset >= m_size || size > m_size - offset || size > m_used)
return false;
auto next = m_free.lower_bound(offset);
if (next != m_free.end() && next->first < offset + size)
return false;
auto prev = next == m_free.begin() ? m_free.end() : std::prev(next);
if (prev != m_free.end() && prev->first + prev->second > offset)
return false;
uint64_t start = offset, end = offset + size;
if (prev != m_free.end() && prev->first + prev->second == start)
{
start = prev->first;
m_free.erase(prev);
}
if (next != m_free.end() && next->first == end)
{
end = next->first + next->second;
m_free.erase(next);
}
m_free.emplace(start, end - start);
m_used -= size;
m_ranges--;
return true;
}
uint64_t Size() const { return m_size; }
uint64_t Used() const { return m_used; } // bytes in ranges handed out
uint32_t Ranges() const { return m_ranges; } // ranges handed out
bool Empty() const { return m_ranges == 0; }
size_t FreeRanges() const { return m_free.size(); }
uint64_t LargestFree() const
{
uint64_t largest = 0;
for (const auto& [offset, size] : m_free)
largest = size > largest ? size : largest;
return largest;
}
// offset -> size, merged: no two touch.
const std::map<uint64_t, uint64_t>& FreeList() const { return m_free; }
private:
std::map<uint64_t, uint64_t> m_free;
uint64_t m_size, m_used = 0;
uint32_t m_ranges = 0;
};
}
File diff suppressed because it is too large. Load diff
+35
View File
@@ -0,0 +1,35 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Xenos command processor (PM4). Phase 5 step 1: a NULL GPU. It executes the
// packets that synchronise the CPU and GPU (register writes, waits, fences,
// memory writes, interrupts, swaps, indirect buffers, constants) and counts
// but skips draws. Packet semantics follow Xenia's gpu/command_processor.cc
// (BSD); its null backend works the same way.
//
// The game's stores to GPU registers land in guest memory at 0x7FC80000 +
// index*4 (we have no MMIO traps), so the write pointer is polled there and
// the register file is mirrored back into that page for the game's reads.
#pragma once
#include <cstdint>
#include <string>
namespace gpu
{
constexpr uint32_t REGISTER_COUNT = 0x5003;
constexpr uint32_t MMIO_BASE = 0x7FC80000;
void InitializeRingBuffer(uint32_t guestAddress, uint32_t sizeLog2);
void EnableReadPointerWriteBack(uint32_t guestAddress, uint32_t blockSizeLog2);
void SetInterruptCallback(uint32_t callback, uint32_t userData);
// Frames completed (XE_SWAP packets executed).
uint64_t FrameCount();
// For the hang watchdog (report/watchdog.cpp), any thread: since when
// (steady clock, ns) the command processor has been polling one
// WAIT_REG_MEM past its first check; 0 when it isn't.
int64_t WaitingSince();
// For a hang report: that wait (with the value it reads now), frame and
// ring positions, and the last 64 packets.
std::string DescribeState();
}
+76
View File
@@ -0,0 +1,76 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See gpu_clock.h.
#include <stdafx.h>
#include "gpu_clock.h"
namespace gpu::clock
{
namespace
{
// trans_stat: a row per frequency, "[*] <Hz>: <transitions...> <ms at it>".
std::map<uint64_t, uint64_t> ReadTimes(const std::filesystem::path& file)
{
std::map<uint64_t, uint64_t> times;
FILE* f = fopen(file.c_str(), "r");
if (!f)
return times;
char line[1024];
while (fgets(line, sizeof(line), f))
{
const char* p = line;
while (*p == ' ' || *p == '*')
p++;
char* end;
unsigned long long hz = strtoull(p, &end, 10);
if (end == p || *end != ':')
continue;
unsigned long long last = 0;
for (p = end + 1;;)
{
unsigned long long v = strtoull(p, &end, 10);
if (end == p)
break;
last = v;
p = end;
}
times[hz] = last;
}
fclose(f);
return times;
}
std::filesystem::path FindTransStat()
{
#ifdef __linux__
std::error_code ec;
for (std::filesystem::directory_iterator it("/sys/class/devfreq", ec), end; !ec && it != end; it.increment(ec))
{
std::string name = it->path().filename().string();
if (name.find("gpu") != std::string::npos && std::filesystem::exists(it->path() / "trans_stat", ec))
return it->path() / "trans_stat";
}
#endif
return {};
}
}
double AverageMHzSinceLast()
{
static const std::filesystem::path file = FindTransStat();
static std::map<uint64_t, uint64_t> last;
if (file.empty())
return 0.0;
std::map<uint64_t, uint64_t> now = ReadTimes(file);
double weighted = 0.0, ms = 0.0;
for (const auto& [hz, t] : now)
{
auto it = last.find(hz);
if (it == last.end() || t < it->second)
continue;
weighted += double(hz) * double(t - it->second);
ms += double(t - it->second);
}
bool first = last.empty();
last = std::move(now);
return first || ms <= 0.0 ? 0.0 : weighted / ms / 1e6;
}
}
+14
View File
@@ -0,0 +1,14 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The GPU's clock for the [perf] line, where the kernel keeps the time spent
// at each frequency (devfreq's trans_stat: the Steam Frame's Adreno, under
// /sys/class/devfreq/<...>.gpu). It tells a GPU that is slower because it
// clocks down (heat, power) from one that is shared (the headset's compositor).
#pragma once
namespace gpu::clock
{
// The average clock since the previous call, in MHz; 0 where unknown
// (no devfreq GPU: amdgpu, macOS) and on the first call.
double AverageMHzSinceLast();
}
+167
View File
@@ -0,0 +1,167 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// xboxkrnl video exports (Vd*). Semantics follow Xenia's xboxkrnl_video.cc
// (BSD): mode queries report 1280x720 widescreen HD at 60 Hz; VdSwap writes
// the front buffer's texture fetch constant and an XE_SWAP packet into the
// 64-dword slot the game reserved in its ring buffer.
#include <stdafx.h>
#include <kernel/function.h>
#include <kernel/vmem.h>
#include <report/report.h>
#include "command_processor.h"
void VdQueryVideoMode(XVIDEO_MODE* mode)
{
memset(mode, 0, sizeof(XVIDEO_MODE));
mode->DisplayWidth = 1280;
mode->DisplayHeight = 720;
mode->IsInterlaced = 0;
mode->IsWidescreen = 1;
mode->IsHighDefinition = 1;
mode->RefreshRate = 0x42700000; // 60.0f
mode->VideoStandard = 1; // NTSC
mode->Unknown4A = 0x4A;
mode->Unknown01 = 0x01;
}
uint32_t VdQueryVideoFlags()
{
return 1 | 2; // widescreen, width >= 1024
}
void VdGetCurrentDisplayGamma(be<uint32_t>* type, be<float>* power)
{
*type = 2; // Xenia's default kernel_display_gamma_type
*power = 2.22222233f;
}
void VdGetCurrentDisplayInformation(uint8_t* info)
{
// X_DISPLAY_INFO (0x58 bytes), filled as Xenia does for 1280x720.
memset(info, 0, 0x58);
auto u16 = [&](uint32_t off, uint16_t v) { *reinterpret_cast<be<uint16_t>*>(info + off) = v; };
auto u32 = [&](uint32_t off, uint32_t v) { *reinterpret_cast<be<uint32_t>*>(info + off) = v; };
u16(0x00, 1280); u16(0x02, 720); // front buffer
u32(0x08 + 0x08, 1280); u32(0x08 + 0x0C, 720); // scaler source rect x2, y2
u32(0x08 + 0x10, 1280); u32(0x08 + 0x14, 720); // scaled output
u32(0x08 + 0x18, 1); // vertical filter type
u32(0x08 + 0x28, 1); // horizontal filter type
u16(0x40, 320); u16(0x42, 180); u16(0x44, 320); u16(0x46, 180); // overscan
u16(0x48, 1280); u16(0x4A, 720);
u32(0x4C, 0x42700000); // 60.0f
u16(0x56, 1280);
}
uint32_t VdSetDisplayMode(uint32_t flags) { return 0; }
uint32_t VdInitializeEngines(uint32_t unk0, uint32_t callback, uint32_t arg, uint32_t pfp, uint32_t me) { return 1; }
void VdShutdownEngines() {}
uint32_t VdGetGraphicsAsicID() { return 0x11; }
uint32_t VdEnableDisableClockGating(uint32_t enabled) { return 0; }
uint32_t VdIsHSIOTrainingSucceeded() { return 1; }
uint32_t VdRetrainEDRAMWorker(uint32_t unk0) { return 0; }
uint32_t VdRetrainEDRAM(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f) { return 0; }
uint32_t VdInitializeEDRAM() { return 0; }
void VdSetSystemCommandBufferGpuIdentifierAddress(uint32_t address) {}
uint32_t VdCallGraphicsNotificationRoutines(uint32_t unk0, void* args) { return 0; }
void VdSetGraphicsInterruptCallback(uint32_t callback, uint32_t userData)
{
gpu::SetInterruptCallback(callback, userData);
}
void VdInitializeRingBuffer(uint32_t ptr, int32_t sizeLog2)
{
gpu::InitializeRingBuffer(ptr, uint32_t(sizeLog2));
}
void VdEnableRingBufferRPtrWriteBack(uint32_t ptr, int32_t blockSizeLog2)
{
gpu::EnableReadPointerWriteBack(ptr, uint32_t(blockSizeLog2));
}
void VdGetSystemCommandBuffer(be<uint32_t>* p0, be<uint32_t>* p1)
{
memset(p0, 0, 0x94);
*p0 = 0xBEEF0000;
*p1 = 0xBEEF0001;
}
uint32_t VdQuerySystemCommandBuffer(be<uint32_t>* p0, be<uint32_t>* p1)
{
VdGetSystemCommandBuffer(p0, p1);
return 0;
}
uint32_t VdSetSystemCommandBuffer(uint32_t unk) { return 0; }
uint32_t VdInitializeScalerCommandBuffer(uint32_t sourceXY, uint32_t sourceWH, uint32_t outputXY, uint32_t outputWH,
uint32_t frontBufferWH, uint32_t verticalFilter, void* verticalParams, uint32_t horizontalFilter,
void* horizontalParams, void* unk9, be<uint32_t>* dest, uint32_t destCount)
{
for (uint32_t i = 0; i < destCount; i++)
dest[i] = 0x80000000; // type-2 filler packets
return destCount;
}
uint32_t VdPersistDisplay(uint32_t unk0, be<uint32_t>* out)
{
if (out)
*out = vmem::Physical().Alloc(64, 32, vmem::X_MEM_RESERVE | vmem::X_MEM_COMMIT, 0x01, false);
return 1;
}
void VdSwap(be<uint32_t>* buffer, be<uint32_t>* fetch, void* unk2, void* unk3, void* unk4,
be<uint32_t>* frontBuffer, be<uint32_t>* textureFormat, be<uint32_t>* colorSpace,
be<uint32_t>* width, be<uint32_t>* height)
{
// NFSMW_TEST_CRASH: a crash with the game's render loop on the stack.
if (report::g_testCrash.load(std::memory_order_relaxed)) [[unlikely]]
report::TestCrash();
// The front buffer's texture fetch constant (6 dwords), with its base
// address rewritten to a physical page number.
uint32_t words[6];
for (int i = 0; i < 6; i++)
words[i] = fetch[i];
uint32_t frontVirtual = (words[1] >> 12) << 12; // dword_1: base_address in bits 12-31
uint32_t frontPhysical = frontVirtual & 0x1FFFFFFF;
words[1] = (words[1] & 0xFFF) | frontPhysical;
memset(buffer, 0, 64 * 4);
uint32_t o = 0;
buffer[o++] = (0u << 30) | ((6 - 1) << 16) | 0x4800; // type 0: SHADER_CONSTANT_FETCH_00_0, 6 regs
for (int i = 0; i < 6; i++)
buffer[o++] = words[i];
buffer[o++] = (3u << 30) | ((4 - 1) << 16) | (0x64 << 8); // type 3: XE_SWAP, 4 dwords
buffer[o++] = 0x53574150; // 'SWAP'
buffer[o++] = frontPhysical;
buffer[o++] = *width;
buffer[o++] = *height;
while (o < 64)
buffer[o++] = 2u << 30; // type-2 filler
}
GUEST_FUNCTION_HOOK(__imp__VdQueryVideoMode, VdQueryVideoMode);
GUEST_FUNCTION_HOOK(__imp__XGetVideoMode, VdQueryVideoMode);
GUEST_FUNCTION_HOOK(__imp__VdQueryVideoFlags, VdQueryVideoFlags);
GUEST_FUNCTION_HOOK(__imp__VdGetCurrentDisplayGamma, VdGetCurrentDisplayGamma);
GUEST_FUNCTION_HOOK(__imp__VdGetCurrentDisplayInformation, VdGetCurrentDisplayInformation);
GUEST_FUNCTION_HOOK(__imp__VdSetDisplayMode, VdSetDisplayMode);
GUEST_FUNCTION_HOOK(__imp__VdInitializeEngines, VdInitializeEngines);
GUEST_FUNCTION_HOOK(__imp__VdShutdownEngines, VdShutdownEngines);
GUEST_FUNCTION_HOOK(__imp__VdGetGraphicsAsicID, VdGetGraphicsAsicID);
GUEST_FUNCTION_HOOK(__imp__VdEnableDisableClockGating, VdEnableDisableClockGating);
GUEST_FUNCTION_HOOK(__imp__VdIsHSIOTrainingSucceeded, VdIsHSIOTrainingSucceeded);
GUEST_FUNCTION_HOOK(__imp__VdRetrainEDRAMWorker, VdRetrainEDRAMWorker);
GUEST_FUNCTION_HOOK(__imp__VdRetrainEDRAM, VdRetrainEDRAM);
GUEST_FUNCTION_HOOK(__imp__VdInitializeEDRAM, VdInitializeEDRAM);
GUEST_FUNCTION_HOOK(__imp__VdSetSystemCommandBufferGpuIdentifierAddress, VdSetSystemCommandBufferGpuIdentifierAddress);
GUEST_FUNCTION_HOOK(__imp__VdCallGraphicsNotificationRoutines, VdCallGraphicsNotificationRoutines);
GUEST_FUNCTION_HOOK(__imp__VdSetGraphicsInterruptCallback, VdSetGraphicsInterruptCallback);
GUEST_FUNCTION_HOOK(__imp__VdInitializeRingBuffer, VdInitializeRingBuffer);
GUEST_FUNCTION_HOOK(__imp__VdEnableRingBufferRPtrWriteBack, VdEnableRingBufferRPtrWriteBack);
GUEST_FUNCTION_HOOK(__imp__VdGetSystemCommandBuffer, VdGetSystemCommandBuffer);
GUEST_FUNCTION_HOOK(__imp__VdQuerySystemCommandBuffer, VdQuerySystemCommandBuffer);
GUEST_FUNCTION_HOOK(__imp__VdSetSystemCommandBuffer, VdSetSystemCommandBuffer);
GUEST_FUNCTION_HOOK(__imp__VdInitializeScalerCommandBuffer, VdInitializeScalerCommandBuffer);
GUEST_FUNCTION_HOOK(__imp__VdPersistDisplay, VdPersistDisplay);
GUEST_FUNCTION_HOOK(__imp__VdSwap, VdSwap);
File diff suppressed because it is too large. Load diff
+190
View File
@@ -0,0 +1,190 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Xenos -> Vulkan renderer (Phase 5, milestone 3). Runs on the command
// processor thread: draws and resolves are recorded as the PM4 stream is
// executed and submitted at synchronisation points (Flush).
//
// - Guest physical memory is imported as one storage buffer
// (VK_EXT_external_memory_host), so vertex fetches and resolves read and
// write guest memory directly. Shaders come from gpu/shader_translator.
// - EDRAM is modelled as host render targets keyed by EDRAM base, format and
// pitch. A resolve (a draw in copy mode) converts a rectangle of one into
// guest memory as a tiled texture, through a compute shader, and applies
// the requested clears.
// - Synchronisation: work is submitted without waiting (up to 8 command
// buffers in flight). Fence and memory writes, scratch registers and the
// read pointer are deferred until the GPU has finished the work before
// them (QueueWrite); interrupts and swaps wait for the GPU (Flush).
//
// State translation follows Xenia (BSD): draw_util.cc (viewport, scissor,
// resolve rectangles) and the Vulkan backend's pipeline cache.
#pragma once
#include <cstdint>
#include <cstdlib>
#include <atomic>
#include <chrono>
#include <functional>
#include <string>
#include <vector>
namespace gpu::renderer
{
// `regs` is the command processor's register file (host order). Returns
// false (and every other call is then a no-op) when running headless.
bool Initialize(const uint32_t* regs);
bool Enabled();
// IM_LOAD / IM_LOAD_IMMEDIATE: `words` in guest (big-endian) order.
void LoadShader(uint32_t type, const uint32_t* words, uint32_t dwords);
// DRAW_INDX / DRAW_INDX_2 (VGT_DRAW_INITIATOR, and VGT_DMA_BASE/SIZE for
// indexed draws). In copy mode this is a resolve.
void Draw(uint32_t initiator, uint32_t dmaBase, uint32_t dmaSize);
// Submit recorded work without waiting.
void Submit();
// Submit only if the GPU has nothing in flight (keeps it fed while
// letting consecutive batches share one submission otherwise).
void SubmitIfIdle();
// Idle command processor: submit if the GPU queue is shallow or the
// recording has aged (NFSMW_SUBMIT_DEPTH, NFSMW_SUBMIT_AGE_US).
void SubmitIfDue();
// Submit recorded work and wait until the GPU has finished everything.
void Flush();
// A CPU-visible effect of the command stream (a fence or memory write, the
// read pointer): runs on the completion thread once the GPU has finished
// the submission being recorded (or the last one in flight; immediately
// when idle). Whoever then waits on the CPU must Submit() first.
void QueueWrite(std::function<void()> write);
// EVENT_WRITE_ZPD at `address` (RB_SAMPLE_COUNT_ADDR when it runs), on
// the command processor thread. NFSMW_OCCLUSION=1 (the presenter
// decides: VulkanContext::occlusionCounting) counts the game's occlusion
// queries on the GPU: a BEGIN opens a bracket and returns false (the
// caller stores its zeros, as without counting), an END closes it and
// returns true (its report is stored at GPU completion with the samples
// counted in between: gpu/zpd_report.h). False when not counting.
bool ZpassDone(uint32_t address);
// Any thread (a capture, `what` names it): with NFSMW_OCCLUSION=1, the
// latest END count stored per report address. Nothing otherwise.
void LogOcclusionCounts(const char* what);
// XE_SWAP: convert the front buffer (its texture fetch constant) into an
// image on the GPU and hand it to the presenter. False when headless.
bool PresentFrontBuffer(const uint32_t fetch[6], uint32_t width, uint32_t height);
// After each swap (NFSMW_PASS_PROFILE=<frame> prints that frame's passes).
void OnSwap(uint64_t frame);
// Counters for the periodic log line.
struct Stats
{
uint64_t draws, skipped, resolves, submits, pipelines, uploads, gpuBusyUs, passes, constantUploads, gpuExecUs, tileSkips, snapshotBytes, shadowWaits, shaderCacheHits, pipelineSkips, lateFrames;
};
Stats GetStats();
// The scaled data path (internal resolution above 1x), totals so far:
// scaled texture loads, bytes the scaled untiles wrote (textures and
// the front buffer), samples scaled resolves wrote, and the scaled
// memory's slot allocations, evictions and ranges found stale (1x).
struct ScaledStats
{
uint64_t loads, untiledBytes, resolvedBytes, allocations, evictions, fallbacks;
};
ScaledStats GetScaledStats();
// Device memory held now, in bytes: the renderer's render targets,
// cached textures, scaled resolve memory and front images, and the
// device's local heaps as it counts this process's use and budget
// (VK_EXT_memory_budget; 0 without it). Command processor thread (it
// walks the caches): the [perf] line, to see that live setting changes
// give back what they replace.
// Image memory (renderer.cpp): the blocks images are bound into, the
// bytes of images in them, the spare blocks, and the images with an
// allocation of their own; totals so far of images made and their time,
// of vkAllocateMemory calls on this thread for images and blocks and
// their time, of blocks taken from the spares, blocks allocated here
// for want of one, blocks freed, and blocks the memory thread made.
struct MemoryStats
{
uint64_t targets, textures, scaled, front, deviceUsed, deviceBudget;
uint32_t targetCount, textureCount;
uint64_t blockBytes, rangeBytes, spareBytes, ownBytes;
uint32_t blocks, ranges, spares, own;
uint64_t images, imageNs, allocations, allocationNs, fromSpare, blocksHere, blocksFreed, spareBlocks, spareNs;
};
MemoryStats GetMemoryStats();
// Per-frame averages over the last 30 swaps, for the performance overlay
// (published by the command processor).
struct PerfNumbers
{
float cpMs = 0, gpuMs = 0, draws = 0;
};
PerfNumbers GetPerfNumbers();
void PublishPerfNumbers(const PerfNumbers& numbers);
// At exit: persist caches.
void Shutdown();
// GLSL to SPIR-V (glslang, through the shader disk cache): stage 0
// vertex, 1 fragment, 2 compute. Empty on failure, with `log` set.
std::vector<uint32_t> CompileShader(const std::string& glsl, int stage, std::string& log);
// Once per swap (the command processor's swap-to-swap time, and the
// guest vblanks since the last swap): log where the frame's time went if
// it was late.
void HitchReport(uint64_t frame, double frameMs, uint32_t vblanks);
// Set by the command processor when any shader constant register (float,
// fetch, bool, loop) is written; the next draw uploads constants again.
inline std::atomic<bool> g_constantsDirty{ true };
// CPU-visible writes of the command stream (fences, scratch registers,
// interrupts) happen when the command processor reaches them, as in
// Xenia; the write-watch GPU guard keeps the guest from reusing memory
// the GPU still has to access. NFSMW_EAGER_WRITES=0 defers them to GPU
// completion instead (one frame in flight: ~45 fps in race on the M1 Pro).
inline const bool g_eagerWrites = [] { const char* v = std::getenv("NFSMW_EAGER_WRITES"); return !v || v[0] != '0'; }();
// Command-processor fast paths, all on by default; NFSMW_CP_OPT=<mask>
// keeps only the given ones (0: the plain paths, for A/B checks).
enum : uint32_t
{
CP_OPT_BULK_REGISTERS = 1, // runs of plain register writes copied at once
CP_OPT_FAST_INDICES = 2, // indices converted straight into the ring (16-bit kept)
CP_OPT_TEXTURE_EPOCH = 4, // texture validity rescanned only after some page was written
CP_OPT_PACKED_CONSTANTS = 8, // shaders read only their used constants, packed (shader_translator)
CP_OPT_SKIP_REDUNDANT = 16, // Vulkan state commands skipped when the value is already set
CP_OPT_TEXTURE_CACHE = 32, // texture lookups kept per fetch slot (NFSMW_TEXTURE_CACHE=0)
CP_OPT_SHADER_CACHE = 64, // IM_LOAD shaders found by address (NFSMW_SHADER_CACHE=0)
};
inline const uint32_t g_cpOpt = [] { const char* v = std::getenv("NFSMW_CP_OPT"); return v ? uint32_t(strtoul(v, nullptr, 0)) : ~0u; }();
// Command-processor thread time, for the [perf] line: busy in ring
// batches, i.e. not blocked (WAIT_REG_MEM, the frame throttle, GPU and
// pipeline waits, a suspension: g_cpWaitNs; by kind for hitch reports).
enum CpWait : uint32_t
{
CP_WAIT_REGMEM, // WAIT_REG_MEM (the game's stores, or the GPU's)
CP_WAIT_THROTTLE, // the previous frame's GPU work (one frame in flight)
CP_WAIT_SHADOW, // a submission still reading pages being synced (shadow mode)
CP_WAIT_FLUSH, // all submitted work (Flush)
CP_WAIT_SLOT, // a free command buffer
CP_WAIT_PIPELINE, // an asynchronous pipeline compile
CP_WAIT_SUSPENDED, // the game is suspended (iOS: the app isn't active; guest time stands still)
CP_WAIT_KINDS
};
inline uint64_t g_cpBusyNs = 0, g_cpWaitNs = 0, g_cpWaitKindNs[CP_WAIT_KINDS] = {};
inline thread_local bool t_commandProcessor = false;
struct CpWaitTimer
{
CpWait kind;
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
explicit CpWaitTimer(CpWait k) : kind(k) {}
~CpWaitTimer()
{
if (!t_commandProcessor)
return;
uint64_t ns = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - start).count());
g_cpWaitNs += ns;
g_cpWaitKindNs[kind] += ns;
}
};
}
File diff suppressed because it is too large. Load diff
+60
View File
@@ -0,0 +1,60 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Xenos shader microcode -> GLSL 4.60 (compiled to SPIR-V at runtime).
//
// Semantics follow Xenia's SPIR-V translator (BSD): Shader Model 3 rules for
// multiplication (+-0 * anything = +0), max/min NaN behaviour, predicates,
// kills, address/loop registers, vertex fetch formats and normalisation.
// Instruction encodings come from Xenia's ucode.h (gpu/xenos/).
//
// Resource model (shared by every shader):
// set 0, binding 0 SharedMemory: guest physical memory as uint[] (words
// hold big-endian data; fetches swap per fetch constant)
// set 0, binding 1 XenosConstants: float constants c[512], fetch constants,
// bool and loop constants, draw parameters
// set 1, binding 0/1/2 sampler2D / sampler3D / samplerCube arrays [32],
// indexed by texture fetch constant slot
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace gpu
{
enum class ShaderStage : uint32_t { Vertex = 0, Pixel = 1 };
struct TranslatedShader
{
bool ok = false;
std::string error; // first translation error, if any
std::string glsl;
uint32_t registerCount = 0;
uint32_t interpolatorMask = 0; // VS: written; PS: read
uint32_t colorTargetMask = 0; // PS: oC# written
bool writesDepth = false;
bool usesKill = false;
bool usesMemExport = false;
uint32_t vertexFetchMask = 0; // fetch constant slots used by vfetch (x3 dwords... slot = const index)
uint32_t vertexFetchConstants[3] = {}; // the vertex fetch constants (0..95) vfetch reads
uint32_t texture2DMask = 0; // texture fetch slots used, by dimension
uint32_t texture3DMask = 0;
uint32_t textureCubeMask = 0;
// Float constants the shader reads (by absolute index, 0..255 of its
// stage). Packed: those reads use slots 0..N-1 in index order, so the
// host uploads only the N used constants (not with relative reads).
uint64_t constUsed[4] = {};
bool constRelative = false;
bool constPacked = false;
};
// NFSMW_CP_OPT bit 8 (on by default): translate shaders with packed constants.
bool PackedConstantsEnabled();
// `ucode` is in host byte order (swap the guest's big-endian words first).
TranslatedShader TranslateShader(ShaderStage stage, const uint32_t* ucode, size_t dwordCount);
// The GLSL declarations every translated shader starts with (also used by
// the host to lay out its buffers).
const char* ShaderCommonGlsl();
}
+47
View File
@@ -0,0 +1,47 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Guest memory in the shaders: each that reads or writes it declares
// SharedMemory { uint g_mem[]; } at set 0, binding 0, follows it with one of
// these, and goes through memLoad(word) / memStore(word, value) (word = byte
// address >> 2, below 2^27). Normally that is g_mem itself: the whole 512 MB
// in one binding. Where a device binds less of a storage buffer than that
// (maxStorageBufferRange: 128 MB on Turnip, the Steam Frame's Adreno 750),
// the renderer compiles the shaders with SPLIT_MEMORY and binds the memory
// as four 128 MB parts, binding 0 and 13-15 (renderer.cpp, kSharedPart).
#pragma once
#define GLSL_SHARED_MEMORY_READ R"(
#ifdef SPLIT_MEMORY
layout(std430, set = 0, binding = 13) readonly buffer SharedMemory1 { uint g_mem1[]; };
layout(std430, set = 0, binding = 14) readonly buffer SharedMemory2 { uint g_mem2[]; };
layout(std430, set = 0, binding = 15) readonly buffer SharedMemory3 { uint g_mem3[]; };
uint memLoad(uint w)
{
uint i = w & 0x1FFFFFFu, part = w >> 25;
if (part == 0u) return g_mem[i];
if (part == 1u) return g_mem1[i];
if (part == 2u) return g_mem2[i];
return g_mem3[i];
}
#else
#define memLoad(w) g_mem[w]
#endif
)"
#define GLSL_SHARED_MEMORY_WRITE R"(
#ifdef SPLIT_MEMORY
layout(std430, set = 0, binding = 13) buffer SharedMemory1 { uint g_mem1[]; };
layout(std430, set = 0, binding = 14) buffer SharedMemory2 { uint g_mem2[]; };
layout(std430, set = 0, binding = 15) buffer SharedMemory3 { uint g_mem3[]; };
void memStore(uint w, uint v)
{
uint i = w & 0x1FFFFFFu, part = w >> 25;
if (part == 0u) g_mem[i] = v;
else if (part == 1u) g_mem1[i] = v;
else if (part == 2u) g_mem2[i] = v;
else g_mem3[i] = v;
}
#else
#define memStore(w, v) g_mem[w] = (v)
#endif
)"
File diff suppressed because it is too large. Load diff
+971
View File
@@ -0,0 +1,971 @@
// Ported from Xenia (https://github.com/xenia-project/xenia), src/xenia/gpu/registers.h.
// Copyright Ben Vanik and Xenia contributors, BSD license. Used in SpeedBreaker (GPL-3.0-or-later).
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2025 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_GPU_REGISTERS_H_
#define XENIA_GPU_REGISTERS_H_
#include <cstdint>
#include <cstdlib>
#include "xenia_compat.h"
#include "xenos.h"
// Most 3D registers are the same as in the Qualcomm Adreno 200 (AMD Z430,
// another R400 architecture family chip):
//
// https://github.com/UDOOboard/Kernel_Unico/blob/master/drivers/mxc/amd-gpu/include/reg/yamato/10/yamato_registers.h
// https://github.com/freedreno/amd-gpu/blob/master/include/reg/yamato/10/yamato_registers.h
//
// https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/freedreno/registers/adreno/a2xx.xml
//
// The Adreno 200, however, has various differences in its registers (primarily
// in the render backend, but not limited to that). Before adding the
// definitions from the Adreno 200, see the actual values of those registers set
// by games, and/or test them on the Xenos hardware.
//
// Other useful sources are the register references for later ATI/AMD 3D chips,
// most importantly the R600 - while it has a massive amount of differences,
// it's the closest relative of the R400 architecture that is available on the
// PC. Documentation for newer AMD GPUs, such as Evergreen, Northern Islands,
// and even GCN also can provide details in some cases. The earlier ATI's
// architecture, R3xx/R5xx, has very differently structured registers (although
// some are still very similar), but can provide some historical context.
//
// Display controller register addresses mostly match the M56 ones:
// https://www.x.org/docs/AMD/old/RRG-216M56-03oOEM.pdf
// All unused bits are intentionally declared as named fields for stable
// comparisons when register values are constructed or modified by Xenia itself.
// Only 32-bit types (uint32_t, int32_t, float or enums with uint32_t / int32_t
// as the underlying type) are allowed in the bit fields here, as Visual C++
// restarts packing when a field requires different alignment than the previous
// one.
namespace xe {
namespace gpu {
enum Register {
#define XE_GPU_REGISTER(index, type, name) XE_GPU_REG_##name = index,
#include "register_table.inc"
#undef XE_GPU_REGISTER
};
namespace reg {
/*******************************************************************************
___ ___ _ _ _____ ___ ___ _
/ __/ _ \| \| |_ _| _ \/ _ \| |
| (_| (_) | .` | | | | / (_) | |__
\___\___/|_|\_| |_| |_|_\\___/|____|
*******************************************************************************/
union alignas(uint32_t) COHER_STATUS_HOST {
uint32_t value;
struct {
uint32_t matching_contexts : 8; // +0
uint32_t rb_copy_dest_base_ena : 1; // +8
uint32_t dest_base_0_ena : 1; // +9
uint32_t dest_base_1_ena : 1; // +10
uint32_t dest_base_2_ena : 1; // +11
uint32_t dest_base_3_ena : 1; // +12
uint32_t dest_base_4_ena : 1; // +13
uint32_t dest_base_5_ena : 1; // +14
uint32_t dest_base_6_ena : 1; // +15
uint32_t dest_base_7_ena : 1; // +16
uint32_t _pad_17 : 7; // +17
uint32_t vc_action_ena : 1; // +24
uint32_t tc_action_ena : 1; // +25
uint32_t pglb_action_ena : 1; // +26
uint32_t _pad_27 : 4; // +27
uint32_t status : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_COHER_STATUS_HOST;
};
static_assert_size(COHER_STATUS_HOST, sizeof(uint32_t));
union alignas(uint32_t) WAIT_UNTIL {
uint32_t value;
struct {
uint32_t _pad_0 : 1; // +0
uint32_t wait_re_vsync : 1; // +1
uint32_t wait_fe_vsync : 1; // +2
uint32_t wait_vsync : 1; // +3
uint32_t wait_dsply_id0 : 1; // +4
uint32_t wait_dsply_id1 : 1; // +5
uint32_t wait_dsply_id2 : 1; // +6
uint32_t _pad_7 : 3; // +7
uint32_t wait_cmdfifo : 1; // +10
uint32_t _pad_11 : 3; // +11
uint32_t wait_2d_idle : 1; // +14
uint32_t wait_3d_idle : 1; // +15
uint32_t wait_2d_idleclean : 1; // +16
uint32_t wait_3d_idleclean : 1; // +17
uint32_t _pad_18 : 2; // +18
uint32_t cmdfifo_entries : 4; // +20
uint32_t _pad_24 : 8; // +24
};
static constexpr Register register_index = XE_GPU_REG_WAIT_UNTIL;
};
static_assert_size(WAIT_UNTIL, sizeof(uint32_t));
/*******************************************************************************
___ ___ ___ _ _ ___ _ _ ___ ___ ___
/ __| __/ _ \| | | | __| \| |/ __| __| _ \
\__ \ _| (_) | |_| | _|| .` | (__| _|| /
|___/___\__\_\\___/|___|_|\_|\___|___|_|_\
*******************************************************************************/
union alignas(uint32_t) SQ_PROGRAM_CNTL {
uint32_t value;
struct {
// GPR counts minus 1.
// Ignore the Freedreno a2xx.xml note about the bit 7 for zero registers,
// the fields are 6-bit, not 8-bit, in yamato_registers.h, and games never
// set the bits 7:6.
uint32_t vs_num_reg : 6; // +0, value minus 1
uint32_t _pad_6 : 2; // +6
uint32_t ps_num_reg : 6; // +8, value minus 1
uint32_t _pad_14 : 2; // +14
uint32_t vs_resource : 1; // +16
uint32_t ps_resource : 1; // +17
uint32_t param_gen : 1; // +18
uint32_t gen_index_pix : 1; // +19
// Interpolator output count minus 1.
uint32_t vs_export_count : 4; // +20, value minus 1
xenos::VertexShaderExportMode vs_export_mode : 3; // +24
uint32_t ps_export_mode : 4; // +27
uint32_t gen_index_vtx : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_SQ_PROGRAM_CNTL;
};
static_assert_size(SQ_PROGRAM_CNTL, sizeof(uint32_t));
union alignas(uint32_t) SQ_CONTEXT_MISC {
uint32_t value;
struct {
uint32_t inst_pred_optimize : 1; // +0
uint32_t sc_output_screen_xy : 1; // +1
xenos::SampleControl sc_sample_cntl : 2; // +2
uint32_t _pad_4 : 4; // +4
// Pixel shader interpolator (according to the XNA microcode validator -
// limited to the interpolator count, 16, not the total register count of
// 64) index to write pixel parameters to.
// See https://portal.unifiedpatents.com/ptab/case/IPR2015-00325 Exhibit
// 2039 R400 Sequencer Specification 2.11 (a significantly early version of
// the specification, however) section 19.2 "Sprites/ XY screen coordinates/
// FB information" for additional details.
// * |XY| - position on screen (vPos - the XNA assembler translates ps_3_0
// vPos directly to this, so at least in Direct3D 9 pixel center mode,
// this contains 0, 1, 2, not 0.5, 1.5, 2.5). flto also said in the
// Freedreno IRC that it's .0 even in OpenGL:
// https://dri.freedesktop.org/~cbrill/dri-log/?channel=freedreno&date=2020-04-19
// According to the actual usage, in the final version of the hardware,
// the screen coordinates are passed to the shader directly as floats
// (contrary to what's written in the early 2.11 version of the sequencer
// specification from IPR2015-00325, where the coordinates are specified
// to be 2^23-biased, essentially packed as integers in the low mantissa
// bits of 2^23).
// * On Android, according to LG P705 - checked on the driver V@6.0 AU@
// (CL@3050818) - GL_OES_get_program_binary disassembly, gl_FragCoord.xy
// is |r0.xy| * c221.xy + c222.zw. Though we haven't yet been able to
// dump the actual constant values by exploiting a huge uniform array,
// but flto says c222.zw contains tile offset plus 0.5. It also appears
// that the multiplication by c221.xy is done to flip the direction of
// the Y axis in gl_FragCoord (c221.y is probably -1). According to the
// tests performed with triangles and point sprites, the hardware uses
// the top-left rasterization rule just like Direct3D (tie-breaking
// sample coverage towards gl_FragCoord.-x+y, while Direct3D tie-breaks
// towards VPOS.-x-y), and the R400 / Z430 doesn't seem to have the
// equivalent of R5xx's SC_EDGERULE register for configuring this).
// Also, both OpenGL and apparently Direct3D 9 define the point sprite V
// coordinate to be 0 in the top, and 1 in the bottom (but OpenGL
// gl_FragCoord.y is towards the top, while Direct3D 9's VPOS.y is
// towards the bottom), gl_PointCoord.y is |PsParamGen.w| directly, and
// the R400 / Z430 doesn't appear to have an equivalent of R6xx's
// SPI_INTERP_CONTROL_0::PNT_SPRITE_TOP_1 for toggling the direction.
// So, it looks like the internal screen coordinates in the official
// OpenGL ES 2.0 driver are still top-to-bottom like in Direct3D, but
// gl_FragCoord.y is flipped in the shader code so it's bottom-to-top
// as OpenGL specifies.
// https://docs.microsoft.com/en-us/windows/win32/direct3d9/point-sprites
// * |ZW| - UV within a point sprite, [0, 1]. In OpenGL ES 2.0, this is
// interpreted directly as gl_PointCoord, with the directions matching the
// OpenGL ES 2.0 specification - 0 in the top (towards +gl_FragCoord.y in
// OpenGL ES bottom-to-top screen coordinates - but towards -PsParamGen.y
// likely, see the explanation of gl_FragCoord.xy above), 1 in the bottom
// (towards -gl_FragCoord.y, or +PsParamGen.y likely). The point sprite
// coordinates are exposed differently on the Xbox 360 and the PC
// Direct3D 9 - the Xbox 360 passes the whole PsParamGen register via the
// SPRITETEXCOORD input semantic directly (unlike on the PC, where point
// sprite coordinates are written to XY of TEXCOORD0), and shaders should
// take abs(SPRITETEXCOORD.zw) explicitly.
// https://shawnhargreaves.com/blog/point-sprites-on-xbox.html
// 4D5307F1 has snowflake point sprites with an asymmetric texture.
// * For non-point primitives, according to LG P705, this may be the IJ
// barycentric coordinates, however, it's not yet known how intentional,
// well-defined and reliable this behavior is, and whether any game uses
// it on purpose. Also, the mapping between the vertex indices and the
// order of these coordinates seems to vary possibly depending on the
// positions of the vertices relative to each other even when the
// winding order stays the same. It's also unknown what effect the
// provoking vertex convention has on the order.
// TODO(Triang3l): Research the order, as well as the sampling location, of
// PsParamGen.zw, the behavior (whether they're extrapolated) when the
// center of the pixel is not covered, on the real hardware.
// * Sign bit of X - is front face (according to the disassembly of vFace
// and gl_FrontFacing usage), non-negative for front face, negative for
// back face (used with `rcpc` in shaders to take signedness of 0 into
// account in `cndge`).
// * Sign bit of Y - is the primitive type a point (according to the
// IPR2015-00325 sequencer specification), negative for a point,
// non-negative for other primitive types.
// * Sign bit of Z - is the primitive type a line (according to the
// IPR2015-00325 sequencer specification), negative for a line,
// non-negative for other primitive types.
uint32_t param_gen_pos : 8; // +8
uint32_t perfcounter_ref : 1; // +16
uint32_t yield_optimize : 1; // +17
uint32_t tx_cache_sel : 1; // +18
uint32_t _pad_19 : 13; // +19
};
static constexpr Register register_index = XE_GPU_REG_SQ_CONTEXT_MISC;
};
static_assert_size(SQ_CONTEXT_MISC, sizeof(uint32_t));
union alignas(uint32_t) SQ_INTERPOLATOR_CNTL {
uint32_t value;
struct {
uint32_t param_shade : 16; // +0
// SampleLocation bits - 0 for centroid, 1 for center, if
// SQ_CONTEXT_MISC::sc_sample_cntl is kCentroidsAndCenters.
uint32_t sampling_pattern : 16; // +16
};
static constexpr Register register_index = XE_GPU_REG_SQ_INTERPOLATOR_CNTL;
};
static_assert_size(SQ_INTERPOLATOR_CNTL, sizeof(uint32_t));
union alignas(uint32_t) SQ_VS_CONST {
uint32_t value;
struct {
uint32_t base : 9; // +0
uint32_t _pad_9 : 3; // +9
// Vec4 count minus one.
uint32_t size : 9; // +12, value minus 1
uint32_t _pad_21 : 11; // +21
};
static constexpr Register register_index = XE_GPU_REG_SQ_VS_CONST;
};
static_assert_size(SQ_VS_CONST, sizeof(uint32_t));
// Same as SQ_VS_CONST.
union alignas(uint32_t) SQ_PS_CONST {
uint32_t value;
struct {
uint32_t base : 9; // +0
uint32_t _pad_9 : 3; // +9
// Vec4 count minus one.
uint32_t size : 9; // +12, value minus 1
uint32_t _pad_21 : 11; // +21
};
static constexpr Register register_index = XE_GPU_REG_SQ_PS_CONST;
};
static_assert_size(SQ_PS_CONST, sizeof(uint32_t));
/*******************************************************************************
__ _____ ___ _____ _____ __
\ \ / / __| _ \_ _| __\ \/ /
\ V /| _|| / | | | _| > <
\_/ |___|_|_\ |_| |___/_/\_\
___ ___ ___ _ _ ___ ___ ___ _ _ _ ___
/ __| _ \/ _ \| | | | _ \ __| _ \ /_\ | \| | \
| (_ | / (_) | |_| | _/ _|| / / _ \| .` | |) |
\___|_|_\\___/ \___/|_| |___|_|_\ /_/ \_\_|\_|___/
_____ ___ ___ ___ ___ _ _ _ _____ ___ ___
|_ _| __/ __/ __| __| | | | /_\_ _/ _ \| _ \
| | | _|\__ \__ \ _|| |__| |__ / _ \| || (_) | /
|_| |___|___/___/___|____|____/_/ \_\_| \___/|_|_\
*******************************************************************************/
union alignas(uint32_t) VGT_DMA_SIZE {
uint32_t value;
struct {
uint32_t num_words : 24; // +0
uint32_t _pad_24 : 6; // +24
xenos::Endian swap_mode : 2; // +30
};
static constexpr Register register_index = XE_GPU_REG_VGT_DMA_SIZE;
};
union alignas(uint32_t) VGT_DRAW_INITIATOR {
uint32_t value;
// Has differences from the Adreno 200.
struct {
xenos::PrimitiveType prim_type : 6; // +0
xenos::SourceSelect source_select : 2; // +6
// Adreno 200 replaced this with FACENESS_CULL_SELECT possibly due to the
// removal of tessellation, but on the Xenos this is MAJOR_MODE like on the
// R600, it's set to the explicit mode mainly for tessellated draws in games
// (because VGT_OUTPUT_PATH_CNTL where tessellation is enabled is ignored in
// the implicit major mode).
xenos::MajorMode major_mode : 2; // +8
uint32_t _pad_10 : 1; // +10
xenos::IndexFormat index_size : 1; // +11
uint32_t not_eop : 1; // +12
uint32_t _pad_13 : 3; // +13
uint32_t num_indices : 16; // +16
};
static constexpr Register register_index = XE_GPU_REG_VGT_DRAW_INITIATOR;
};
static_assert_size(VGT_DRAW_INITIATOR, sizeof(uint32_t));
// Unlike on R6xx (but closer to R5xx), and according to the Adreno 200 header,
// the registers related to the vertex index are 24-bit. Vertex indices are
// unsigned, and only the lower 24 bits of them are actually used by the GPU -
// this has been verified on an Adreno 200 phone (LG Optimus L7) on OpenGL ES
// using a GL_UNSIGNED_INT element array buffer with junk in the upper 8 bits
// that had no effect on drawing.
// The order of operations is primitive reset index checking -> offsetting ->
// clamping.
union alignas(uint32_t) VGT_MULTI_PRIM_IB_RESET_INDX {
uint32_t value;
struct {
// The upper 8 bits of the value from the index buffer are confirmed to be
// ignored. So, though this specifically is untested (because
// GL_PRIMITIVE_RESTART_FIXED_INDEX was added only in OpenGL ES 3.0, though
// it behaves conceptually close to our expectations anyway - uses the
// 0xFFFFFFFF restart index while GL_MAX_ELEMENT_INDEX may be 0xFFFFFF),
// the restart index check likely only involves the lower 24 bit of the
// vertex index - therefore, if reset_indx is 0xFFFFFF, likely 0xFFFFFF,
// 0x1FFFFFF, 0xFFFFFFFF all cause primitive reset.
uint32_t reset_indx : 24; // +0
uint32_t _pad_24 : 8; // +24
};
static constexpr Register register_index =
XE_GPU_REG_VGT_MULTI_PRIM_IB_RESET_INDX;
};
static_assert_size(VGT_MULTI_PRIM_IB_RESET_INDX, sizeof(uint32_t));
union alignas(uint32_t) VGT_INDX_OFFSET {
uint32_t value;
struct {
// Unlike R5xx's VAP_INDEX_OFFSET, which is signed 25-bit, this is 24-bit -
// and signedness doesn't matter as index calculations are done in 24-bit
// integers, and ((0xFFFFFE + 3) & 0xFFFFFF) == 1 anyway, just like
// ((0xFFFFFFFE + 3) & 0xFFFFFF) == 1 if we treated it as signed by
// sign-extending on the host. Direct3D 9 just writes BaseVertexIndex as a
// signed int32 to the entire register, but the upper 8 bits are ignored
// anyway, and that has no effect on offsets that fit in 24 bits.
uint32_t indx_offset : 24; // +0
uint32_t _pad_24 : 8; // +24
};
static constexpr Register register_index = XE_GPU_REG_VGT_INDX_OFFSET;
};
static_assert_size(VGT_INDX_OFFSET, sizeof(uint32_t));
union alignas(uint32_t) VGT_MIN_VTX_INDX {
uint32_t value;
struct {
uint32_t min_indx : 24; // +0
uint32_t _pad_24 : 8; // +24
};
static constexpr Register register_index = XE_GPU_REG_VGT_MIN_VTX_INDX;
};
static_assert_size(VGT_MIN_VTX_INDX, sizeof(uint32_t));
union alignas(uint32_t) VGT_MAX_VTX_INDX {
uint32_t value;
struct {
// Usually 0xFFFF or 0xFFFFFF.
uint32_t max_indx : 24; // +0
uint32_t _pad_24 : 8; // +24
};
static constexpr Register register_index = XE_GPU_REG_VGT_MAX_VTX_INDX;
};
static_assert_size(VGT_MAX_VTX_INDX, sizeof(uint32_t));
union alignas(uint32_t) VGT_OUTPUT_PATH_CNTL {
uint32_t value;
struct {
xenos::VGTOutputPath path_select : 2; // +0
uint32_t _pad_2 : 30; // +2
};
static constexpr Register register_index = XE_GPU_REG_VGT_OUTPUT_PATH_CNTL;
};
static_assert_size(VGT_OUTPUT_PATH_CNTL, sizeof(uint32_t));
union alignas(uint32_t) VGT_HOS_CNTL {
uint32_t value;
struct {
xenos::TessellationMode tess_mode : 2; // +0
uint32_t _pad_2 : 30; // +2
};
static constexpr Register register_index = XE_GPU_REG_VGT_HOS_CNTL;
};
static_assert_size(VGT_HOS_CNTL, sizeof(uint32_t));
/*******************************************************************************
___ ___ ___ __ __ ___ _____ _____ _____
| _ \ _ \_ _| \/ |_ _|_ _|_ _\ \ / / __|
| _/ /| || |\/| || | | | | | \ V /| _|
|_| |_|_\___|_| |_|___| |_| |___| \_/ |___|
_ ___ ___ ___ __ __ ___ _ ___ ___
/_\ / __/ __| __| \/ | _ ) | | __| _ \
/ _ \\__ \__ \ _|| |\/| | _ \ |__| _|| /
/_/ \_\___/___/___|_| |_|___/____|___|_|_\
*******************************************************************************/
union alignas(uint32_t) PA_SU_POINT_MINMAX {
uint32_t value;
struct {
// For per-vertex size specification, radius (1/2 size), 12.4 fixed point.
uint32_t min_size : 16; // +0
uint32_t max_size : 16; // +16
};
static constexpr Register register_index = XE_GPU_REG_PA_SU_POINT_MINMAX;
};
static_assert_size(PA_SU_POINT_MINMAX, sizeof(uint32_t));
union alignas(uint32_t) PA_SU_POINT_SIZE {
uint32_t value;
struct {
// 1/2 width or height, 12.4 fixed point.
uint32_t height : 16; // +0
uint32_t width : 16; // +16
};
static constexpr Register register_index = XE_GPU_REG_PA_SU_POINT_SIZE;
};
static_assert_size(PA_SU_POINT_SIZE, sizeof(uint32_t));
// Setup Unit / Scanline Converter mode cntl
union alignas(uint32_t) PA_SU_SC_MODE_CNTL {
uint32_t value;
struct {
uint32_t cull_front : 1; // +0
uint32_t cull_back : 1; // +1
// 0 - front is CCW, 1 - front is CW.
uint32_t face : 1; // +2
// 4541096E uses poly_mode 2 for triangles, which is "reserved" on R6xx and
// not defined on Adreno 2xx, but polymode_front/back_ptype are 0 (points)
// in this case in 4541096E, which should not be respected for non-kDualMode
// as the title wants to draw filled triangles.
xenos::PolygonModeEnable poly_mode : 2; // +3
xenos::PolygonType polymode_front_ptype : 3; // +5
xenos::PolygonType polymode_back_ptype : 3; // +8
uint32_t poly_offset_front_enable : 1; // +11
uint32_t poly_offset_back_enable : 1; // +12
uint32_t poly_offset_para_enable : 1; // +13
uint32_t _pad_14 : 1; // +14
uint32_t msaa_enable : 1; // +15
uint32_t vtx_window_offset_enable : 1; // +16
// LINE_STIPPLE_ENABLE was added on Adreno.
uint32_t _pad_17 : 2; // +17
uint32_t provoking_vtx_last : 1; // +19
uint32_t persp_corr_dis : 1; // +20
uint32_t multi_prim_ib_ena : 1; // +21
uint32_t _pad_22 : 1; // +22
uint32_t quad_order_enable : 1; // +23
uint32_t sc_one_quad_per_clock : 1; // +24
// WAIT_RB_IDLE_ALL_TRI and WAIT_RB_IDLE_FIRST_TRI_NEW_STATE were added on
// Adreno.
uint32_t _pad_25 : 7; // +25
};
static constexpr Register register_index = XE_GPU_REG_PA_SU_SC_MODE_CNTL;
};
static_assert_size(PA_SU_SC_MODE_CNTL, sizeof(uint32_t));
// Setup Unit Vertex Control
union alignas(uint32_t) PA_SU_VTX_CNTL {
uint32_t value;
struct {
xenos::PixelCenter pix_center : 1; // +0
xenos::VertexRounding round_mode : 2; // +1
xenos::VertexQuantization quant_mode : 3; // +3
uint32_t _pad_6 : 26; // +6
};
static constexpr Register register_index = XE_GPU_REG_PA_SU_VTX_CNTL;
};
static_assert_size(PA_SU_VTX_CNTL, sizeof(uint32_t));
union alignas(uint32_t) PA_SC_MPASS_PS_CNTL {
uint32_t value;
struct {
uint32_t mpass_pix_vec_per_pass : 20; // +0
uint32_t _pad_20 : 11; // +20
uint32_t mpass_ps_ena : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_MPASS_PS_CNTL;
};
static_assert_size(PA_SC_MPASS_PS_CNTL, sizeof(uint32_t));
// Scanline converter viz query, used by D3D for gpu side conditional rendering
union alignas(uint32_t) PA_SC_VIZ_QUERY {
uint32_t value;
struct {
// the visibility of draws should be evaluated
uint32_t viz_query_ena : 1; // +0
uint32_t viz_query_id : 6; // +1
// discard geometry after test (but use for testing)
uint32_t kill_pix_post_hi_z : 1; // +7
// not used with d3d
uint32_t kill_pix_post_detail_mask : 1; // +8
uint32_t _pad_9 : 23; // +9
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_VIZ_QUERY;
};
static_assert_size(PA_SC_VIZ_QUERY, sizeof(uint32_t));
// Clipper clip control
union alignas(uint32_t) PA_CL_CLIP_CNTL {
uint32_t value;
struct {
// Like on the Adreno 200, but with user clip planes from R3xx (used in
// 4D5307E6 for the hanging lamp on Last Resort).
uint32_t ucp_ena_0 : 1; // +0
uint32_t ucp_ena_1 : 1; // +1
uint32_t ucp_ena_2 : 1; // +2
uint32_t ucp_ena_3 : 1; // +3
uint32_t ucp_ena_4 : 1; // +4
uint32_t ucp_ena_5 : 1; // +5
uint32_t _pad_6 : 8; // +6
uint32_t ps_ucp_mode : 2; // +14
uint32_t clip_disable : 1; // +16
uint32_t ucp_cull_only_ena : 1; // +17
uint32_t boundary_edge_flag_ena : 1; // +18
uint32_t dx_clip_space_def : 1; // +19
uint32_t dis_clip_err_detect : 1; // +20
uint32_t vtx_kill_or : 1; // +21
uint32_t xy_nan_retain : 1; // +22
uint32_t z_nan_retain : 1; // +23
uint32_t w_nan_retain : 1; // +24
uint32_t _pad_25 : 7; // +25
};
struct {
uint32_t ucp_ena : 6;
};
static constexpr Register register_index = XE_GPU_REG_PA_CL_CLIP_CNTL;
};
static_assert_size(PA_CL_CLIP_CNTL, sizeof(uint32_t));
// Viewport transform engine control
union alignas(uint32_t) PA_CL_VTE_CNTL {
uint32_t value;
struct {
uint32_t vport_x_scale_ena : 1; // +0
uint32_t vport_x_offset_ena : 1; // +1
uint32_t vport_y_scale_ena : 1; // +2
uint32_t vport_y_offset_ena : 1; // +3
uint32_t vport_z_scale_ena : 1; // +4
uint32_t vport_z_offset_ena : 1; // +5
uint32_t _pad_6 : 2; // +6
uint32_t vtx_xy_fmt : 1; // +8
uint32_t vtx_z_fmt : 1; // +9
uint32_t vtx_w0_fmt : 1; // +10
uint32_t perfcounter_ref : 1; // +11
uint32_t _pad_12 : 20; // +12
};
static constexpr Register register_index = XE_GPU_REG_PA_CL_VTE_CNTL;
};
static_assert_size(PA_CL_VTE_CNTL, sizeof(uint32_t));
union alignas(uint32_t) PA_SC_SCREEN_SCISSOR_TL {
uint32_t value;
struct {
int32_t tl_x : 15; // +0
uint32_t _pad_15 : 1; // +15
int32_t tl_y : 15; // +16
uint32_t _pad_31 : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_SCREEN_SCISSOR_TL;
};
static_assert_size(PA_SC_SCREEN_SCISSOR_TL, sizeof(uint32_t));
union alignas(uint32_t) PA_SC_SCREEN_SCISSOR_BR {
uint32_t value;
struct {
int32_t br_x : 15; // +0
uint32_t _pad_15 : 1; // +15
int32_t br_y : 15; // +16
uint32_t _pad_31 : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_SCREEN_SCISSOR_BR;
};
static_assert_size(PA_SC_SCREEN_SCISSOR_BR, sizeof(uint32_t));
union alignas(uint32_t) PA_SC_WINDOW_OFFSET {
uint32_t value;
struct {
int32_t window_x_offset : 15; // +0
uint32_t _pad_15 : 1; // +15
int32_t window_y_offset : 15; // +16
uint32_t _pad_31 : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_WINDOW_OFFSET;
};
static_assert_size(PA_SC_WINDOW_OFFSET, sizeof(uint32_t));
union alignas(uint32_t) PA_SC_WINDOW_SCISSOR_TL {
uint32_t value;
struct {
uint32_t tl_x : 14; // +0
uint32_t _pad_14 : 2; // +14
uint32_t tl_y : 14; // +16
uint32_t _pad_30 : 1; // +30
uint32_t window_offset_disable : 1; // +31
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_WINDOW_SCISSOR_TL;
};
static_assert_size(PA_SC_WINDOW_SCISSOR_TL, sizeof(uint32_t));
union alignas(uint32_t) PA_SC_WINDOW_SCISSOR_BR {
uint32_t value;
struct {
uint32_t br_x : 14; // +0
uint32_t _pad_14 : 2; // +14
uint32_t br_y : 14; // +16
uint32_t _pad_30 : 2; // +30
};
static constexpr Register register_index = XE_GPU_REG_PA_SC_WINDOW_SCISSOR_BR;
};
static_assert_size(PA_SC_WINDOW_SCISSOR_BR, sizeof(uint32_t));
/*******************************************************************************
___ ___ _ _ ___ ___ ___
| _ \ __| \| | \| __| _ \
| / _|| .` | |) | _|| /
|_|_\___|_|\_|___/|___|_|_\
___ _ ___ _ _____ _ _ ___
| _ ) /_\ / __| |/ / __| \| | \
| _ \/ _ \ (__| ' <| _|| .` | |) |
|___/_/ \_\___|_|\_\___|_|\_|___/
*******************************************************************************/
union alignas(uint32_t) RB_MODECONTROL {
uint32_t value;
struct {
xenos::EdramMode edram_mode : 3; // +0
uint32_t _pad_3 : 29; // +3
};
static constexpr Register register_index = XE_GPU_REG_RB_MODECONTROL;
};
static_assert_size(RB_MODECONTROL, sizeof(uint32_t));
union alignas(uint32_t) RB_SURFACE_INFO {
uint32_t value;
struct {
uint32_t surface_pitch : 14; // +0 in pixels.
uint32_t _pad_14 : 2; // +14
xenos::MsaaSamples msaa_samples : 2; // +16
uint32_t hiz_pitch : 14; // +18
};
static constexpr Register register_index = XE_GPU_REG_RB_SURFACE_INFO;
};
static_assert_size(RB_SURFACE_INFO, sizeof(uint32_t));
union alignas(uint32_t) RB_COLORCONTROL {
uint32_t value;
struct {
xenos::CompareFunction alpha_func : 3; // +0
uint32_t alpha_test_enable : 1; // +3
uint32_t alpha_to_mask_enable : 1; // +4
// Everything in between was added on Adreno.
uint32_t _pad_5 : 19; // +5
// TODO(Triang3l): Redo these tests and possibly flip these vertically in
// the comment and in the actual implementation. It appears that
// gl_FragCoord.y is mirrored as opposed to the actual screen coordinates in
// the rasterizer (see the SQ_CONTEXT_MISC::param_gen_pos comment here).
// According to tests on an Adreno 200 device (LG Optimus L7), done by
// drawing 0.5x0.5 rectangles in different corners of four pixels in a quad
// to a multisampled GLSurfaceView, the coverage mask is the following for 4
// samples:
// 0.25) [0.25, 0.5) [0.5, 0.75) [0.75, 1) [1
// -- -- -- -- --
// | | | | | #| |##| |##|
// | | |# | |# | |# | |##|
// -- -- -- -- --
// (gl_FragCoord.y near 0 in the top, near 1 in the bottom here - D3D-like.)
// For 2 samples, the top sample (closer to gl_FragCoord.y 0) is covered
// when alpha is in [0.5, 1), the bottom sample is covered when the alpha is
// [1. With these thresholds, however, in 5454082B, almost all distant trees
// are transparent, this is asymmetric - fully transparent for a quarter of
// the range (or even half of the range for 2x and almost the entire range
// for 1x), but fully opaque only in one value.
// Though, 2, 2, 2, 2 offset values are commonly used for undithered alpha
// to coverage (in games such as 5454082B, and overall in AMD driver
// implementations) - it appears that 2, 2, 2, 2 offsets are supposed to
// make this symmetric.
// Both 5454082B and RADV (which used AMDVLK as a reference) use 3, 1, 0, 2
// offsets for dithered alpha to mask.
// https://gitlab.freedesktop.org/nchery/mesa/commit/8a52e4cc4fad4f1c75acc0badd624778f9dfe202
// It appears that the offsets lower the thresholds by (offset / 4 /
// sample count). That's consistent with both 2, 2, 2, 2 making the test
// symmetric and 0, 0, 0, 0 (forgetting to set the offset values) resulting
// in what the official Adreno 200 driver for Android (which is pretty buggy
// overall) produces.
// According to Evergreen register reference:
// - offset0 is for pixel (0, 0) in each quad.
// - offset1 is for pixel (0, 1) in each quad.
// - offset2 is for pixel (1, 0) in each quad.
// - offset3 is for pixel (1, 1) in each quad.
uint32_t alpha_to_mask_offset0 : 2; // +24
uint32_t alpha_to_mask_offset1 : 2; // +26
uint32_t alpha_to_mask_offset2 : 2; // +28
uint32_t alpha_to_mask_offset3 : 2; // +30
};
static constexpr Register register_index = XE_GPU_REG_RB_COLORCONTROL;
};
static_assert_size(RB_COLORCONTROL, sizeof(uint32_t));
union alignas(uint32_t) RB_COLOR_INFO {
uint32_t value;
struct {
// The original R400 structure has 12-bit color_base, however the Xenos has
// periodic 11-bit EDRAM tile addressing, so this field was split in Xenia
// for convenience and to avoid mistakes.
uint32_t color_base : 11; // +0 in tiles.
uint32_t color_base_bit_11 : 1; // +11
uint32_t _pad_12 : 4; // +12
xenos::ColorRenderTargetFormat color_format : 4; // +16
int32_t color_exp_bias : 6; // +20
uint32_t _pad_26 : 6; // +26
};
static constexpr Register register_index = XE_GPU_REG_RB_COLOR_INFO;
// RB_COLOR[1-3]_INFO also use this format.
static const Register rt_register_indices[4];
};
static_assert_size(RB_COLOR_INFO, sizeof(uint32_t));
union alignas(uint32_t) RB_COLOR_MASK {
uint32_t value;
struct {
uint32_t write_red0 : 1; // +0
uint32_t write_green0 : 1; // +1
uint32_t write_blue0 : 1; // +2
uint32_t write_alpha0 : 1; // +3
uint32_t write_red1 : 1; // +4
uint32_t write_green1 : 1; // +5
uint32_t write_blue1 : 1; // +6
uint32_t write_alpha1 : 1; // +7
uint32_t write_red2 : 1; // +8
uint32_t write_green2 : 1; // +9
uint32_t write_blue2 : 1; // +10
uint32_t write_alpha2 : 1; // +11
uint32_t write_red3 : 1; // +12
uint32_t write_green3 : 1; // +13
uint32_t write_blue3 : 1; // +14
uint32_t write_alpha3 : 1; // +15
uint32_t _pad_16 : 16; // +16
};
static constexpr Register register_index = XE_GPU_REG_RB_COLOR_MASK;
};
static_assert_size(RB_COLOR_MASK, sizeof(uint32_t));
union alignas(uint32_t) RB_BLENDCONTROL {
uint32_t value;
struct {
xenos::BlendFactor color_srcblend : 5; // +0
xenos::BlendOp color_comb_fcn : 3; // +5
xenos::BlendFactor color_destblend : 5; // +8
uint32_t _pad_13 : 3; // +13
xenos::BlendFactor alpha_srcblend : 5; // +16
xenos::BlendOp alpha_comb_fcn : 3; // +21
xenos::BlendFactor alpha_destblend : 5; // +24
// BLEND_FORCE_ENABLE and BLEND_FORCE were added on Adreno.
uint32_t _pad_29 : 3; // +29
};
// RB_BLENDCONTROL[0-3] use this format.
static constexpr Register register_index = XE_GPU_REG_RB_BLENDCONTROL0;
static const Register rt_register_indices[4];
};
static_assert_size(RB_BLENDCONTROL, sizeof(uint32_t));
union alignas(uint32_t) RB_DEPTHCONTROL {
uint32_t value;
struct {
uint32_t stencil_enable : 1; // +0
uint32_t z_enable : 1; // +1
uint32_t z_write_enable : 1; // +2
// EARLY_Z_ENABLE was added on Adreno, never set by Xbox 360 games.
uint32_t _pad_3 : 1; // +3
xenos::CompareFunction zfunc : 3; // +4
uint32_t backface_enable : 1; // +7
xenos::CompareFunction stencilfunc : 3; // +8
xenos::StencilOp stencilfail : 3; // +11
xenos::StencilOp stencilzpass : 3; // +14
xenos::StencilOp stencilzfail : 3; // +17
xenos::CompareFunction stencilfunc_bf : 3; // +20
xenos::StencilOp stencilfail_bf : 3; // +23
xenos::StencilOp stencilzpass_bf : 3; // +26
xenos::StencilOp stencilzfail_bf : 3; // +29
};
static constexpr Register register_index = XE_GPU_REG_RB_DEPTHCONTROL;
};
static_assert_size(RB_DEPTHCONTROL, sizeof(uint32_t));
union alignas(uint32_t) RB_STENCILREFMASK {
uint32_t value;
struct {
uint32_t stencilref : 8; // +0
uint32_t stencilmask : 8; // +8
uint32_t stencilwritemask : 8; // +16
uint32_t _pad_24 : 8; // +24
};
static constexpr Register register_index = XE_GPU_REG_RB_STENCILREFMASK;
// RB_STENCILREFMASK_BF also uses this format.
};
static_assert_size(RB_STENCILREFMASK, sizeof(uint32_t));
union alignas(uint32_t) RB_DEPTH_INFO {
uint32_t value;
struct {
// The original R400 structure has 12-bit depth_base, however the Xenos has
// periodic 11-bit EDRAM tile addressing, so this field was split in Xenia
// for convenience and to avoid mistakes.
uint32_t depth_base : 11; // +0 in tiles.
uint32_t depth_base_bit_11 : 1; // +11
uint32_t _pad_12 : 4; // +12
xenos::DepthRenderTargetFormat depth_format : 1; // +16
uint32_t _pad_17 : 15; // +17
};
static constexpr Register register_index = XE_GPU_REG_RB_DEPTH_INFO;
};
static_assert_size(RB_DEPTH_INFO, sizeof(uint32_t));
// Copy registers are very different than on Adreno.
union alignas(uint32_t) RB_COPY_CONTROL {
uint32_t value;
struct {
uint32_t copy_src_select : 3; // +0 Depth is 4.
uint32_t _pad_3 : 1; // +3
xenos::CopySampleSelect copy_sample_select : 3; // +4
uint32_t _pad_7 : 1; // +7
uint32_t color_clear_enable : 1; // +8
uint32_t depth_clear_enable : 1; // +9
uint32_t _pad_10 : 10; // +10
xenos::CopyCommand copy_command : 2; // +20
uint32_t _pad_22 : 10; // +22
};
static constexpr Register register_index = XE_GPU_REG_RB_COPY_CONTROL;
};
static_assert_size(RB_COPY_CONTROL, sizeof(uint32_t));
union alignas(uint32_t) RB_COPY_DEST_INFO {
uint32_t value;
struct {
xenos::Endian128 copy_dest_endian : 3; // +0
uint32_t copy_dest_array : 1; // +3
uint32_t copy_dest_slice : 3; // +4
xenos::ColorFormat copy_dest_format : 6; // +7
xenos::SurfaceNumberFormat copy_dest_number : 3; // +13
int32_t copy_dest_exp_bias : 6; // +16
uint32_t _pad_22 : 2; // +22
uint32_t copy_dest_swap : 1; // +24
uint32_t _pad_25 : 7; // +25
};
static constexpr Register register_index = XE_GPU_REG_RB_COPY_DEST_INFO;
};
static_assert_size(RB_COPY_DEST_INFO, sizeof(uint32_t));
union alignas(uint32_t) RB_COPY_DEST_PITCH {
uint32_t value;
struct {
uint32_t copy_dest_pitch : 14; // +0
uint32_t _pad_14 : 2; // +14
uint32_t copy_dest_height : 14; // +16
uint32_t _pad_30 : 2; // +30
};
static constexpr Register register_index = XE_GPU_REG_RB_COPY_DEST_PITCH;
};
static_assert_size(RB_COPY_DEST_PITCH, sizeof(uint32_t));
/*******************************************************************************
___ ___ ___ ___ _ ___ __
| \_ _/ __| _ \ | /_\ \ / /
| |) | |\__ \ _/ |__ / _ \ V /
|___/___|___/_| |____/_/ \_\_|
___ ___ _ _ _____ ___ ___ _ _ ___ ___
/ __/ _ \| \| |_ _| _ \/ _ \| | | | | __| _ \
| (_| (_) | .` | | | | / (_) | |__| |__| _|| /
\___\___/|_|\_| |_| |_|_\\___/|____|____|___|_|_\
*******************************************************************************/
union alignas(uint32_t) DC_LUT_RW_INDEX {
uint32_t value;
struct {
// Unlike in the M56 documentation, for the 256-table entry, this is the
// absolute index, without the lower or upper 10 bits selection in the
// bit 0. For PWL, the bit 7 is ignored.
uint32_t rw_index : 8; // +0
uint32_t _pad_8 : 24; // +8
};
static constexpr Register register_index = XE_GPU_REG_DC_LUT_RW_INDEX;
};
static_assert_size(DC_LUT_RW_INDEX, sizeof(uint32_t));
union alignas(uint32_t) DC_LUT_SEQ_COLOR {
uint32_t value;
struct {
uint32_t seq_color : 16; // +0, bits 0:5 are hardwired to zero
uint32_t _pad_16 : 16; // +16
};
static constexpr Register register_index = XE_GPU_REG_DC_LUT_SEQ_COLOR;
};
static_assert_size(DC_LUT_SEQ_COLOR, sizeof(uint32_t));
union alignas(uint32_t) DC_LUT_PWL_DATA {
uint32_t value;
struct {
// See the M56 DC_LUTA_CONTROL for information about the way these should be
// interpreted (`output = base + (multiplier * delta) / 2^increment`, where
// the increment is the value specified in DC_LUTA_CONTROL for the specific
// color channel, the base is 7 bits of the front buffer value above
// `increment` bits, the multiplier is the lower `increment` bits of it; the
// increment is nonzero, otherwise the 256-entry table should be used
// instead).
uint32_t base : 16; // +0, bits 0:5 are hardwired to zero
uint32_t delta : 16; // +16, bits 0:5 are hardwired to zero
};
static constexpr Register register_index = XE_GPU_REG_DC_LUT_PWL_DATA;
};
static_assert_size(DC_LUT_PWL_DATA, sizeof(uint32_t));
union alignas(uint32_t) DC_LUT_30_COLOR {
uint32_t value;
struct {
uint32_t color_10_blue : 10; // +0
uint32_t color_10_green : 10; // +10
uint32_t color_10_red : 10; // +20
uint32_t _pad_30 : 2; // +30
};
static constexpr Register register_index = XE_GPU_REG_DC_LUT_30_COLOR;
};
static_assert_size(DC_LUT_30_COLOR, sizeof(uint32_t));
} // namespace reg
} // namespace gpu
} // namespace xe
#endif // XENIA_GPU_REGISTERS_H_
+122
View File
@@ -0,0 +1,122 @@
// Ported from Xenia (https://github.com/xenia-project/xenia), src/xenia/gpu/ucode.cc.
// Copyright Ben Vanik and Xenia contributors, BSD license. Used in SpeedBreaker (GPL-3.0-or-later).
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2022 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "ucode.h"
namespace xe {
namespace gpu {
namespace ucode {
const AluScalarOpcodeInfo kAluScalarOpcodeInfos[64] = {
{"adds", 1, true, kAluOpChangedStateNone},
{"adds_prev", 1, false, kAluOpChangedStateNone},
{"muls", 1, true, kAluOpChangedStateNone},
{"muls_prev", 1, false, kAluOpChangedStateNone},
{"muls_prev2", 1, true, kAluOpChangedStateNone},
{"maxs", 1, true, kAluOpChangedStateNone},
{"mins", 1, true, kAluOpChangedStateNone},
{"seqs", 1, false, kAluOpChangedStateNone},
{"sgts", 1, false, kAluOpChangedStateNone},
{"sges", 1, false, kAluOpChangedStateNone},
{"snes", 1, false, kAluOpChangedStateNone},
{"frcs", 1, false, kAluOpChangedStateNone},
{"truncs", 1, false, kAluOpChangedStateNone},
{"floors", 1, false, kAluOpChangedStateNone},
{"exp", 1, false, kAluOpChangedStateNone},
{"logc", 1, false, kAluOpChangedStateNone},
{"log", 1, false, kAluOpChangedStateNone},
{"rcpc", 1, false, kAluOpChangedStateNone},
{"rcpf", 1, false, kAluOpChangedStateNone},
{"rcp", 1, false, kAluOpChangedStateNone},
{"rsqc", 1, false, kAluOpChangedStateNone},
{"rsqf", 1, false, kAluOpChangedStateNone},
{"rsq", 1, false, kAluOpChangedStateNone},
{"maxas", 1, true, kAluOpChangedStateAddressRegister},
{"maxasf", 1, true, kAluOpChangedStateAddressRegister},
{"subs", 1, true, kAluOpChangedStateNone},
{"subs_prev", 1, false, kAluOpChangedStateNone},
{"setp_eq", 1, false, kAluOpChangedStatePredicate},
{"setp_ne", 1, false, kAluOpChangedStatePredicate},
{"setp_gt", 1, false, kAluOpChangedStatePredicate},
{"setp_ge", 1, false, kAluOpChangedStatePredicate},
{"setp_inv", 1, false, kAluOpChangedStatePredicate},
{"setp_pop", 1, false, kAluOpChangedStatePredicate},
{"setp_clr", 0, false, kAluOpChangedStatePredicate},
{"setp_rstr", 1, false, kAluOpChangedStatePredicate},
{"kills_eq", 1, false, kAluOpChangedStatePixelKill},
{"kills_gt", 1, false, kAluOpChangedStatePixelKill},
{"kills_ge", 1, false, kAluOpChangedStatePixelKill},
{"kills_ne", 1, false, kAluOpChangedStatePixelKill},
{"kills_one", 1, false, kAluOpChangedStatePixelKill},
{"sqrt", 1, false, kAluOpChangedStateNone},
{"opcode_41", 0, false, kAluOpChangedStateNone},
{"mulsc", 2, false, kAluOpChangedStateNone},
{"mulsc", 2, false, kAluOpChangedStateNone},
{"addsc", 2, false, kAluOpChangedStateNone},
{"addsc", 2, false, kAluOpChangedStateNone},
{"subsc", 2, false, kAluOpChangedStateNone},
{"subsc", 2, false, kAluOpChangedStateNone},
{"sin", 1, false, kAluOpChangedStateNone},
{"cos", 1, false, kAluOpChangedStateNone},
{"retain_prev", 0, false, kAluOpChangedStateNone},
{"opcode_51", 0, false, kAluOpChangedStateNone},
{"opcode_52", 0, false, kAluOpChangedStateNone},
{"opcode_53", 0, false, kAluOpChangedStateNone},
{"opcode_54", 0, false, kAluOpChangedStateNone},
{"opcode_55", 0, false, kAluOpChangedStateNone},
{"opcode_56", 0, false, kAluOpChangedStateNone},
{"opcode_57", 0, false, kAluOpChangedStateNone},
{"opcode_58", 0, false, kAluOpChangedStateNone},
{"opcode_59", 0, false, kAluOpChangedStateNone},
{"opcode_60", 0, false, kAluOpChangedStateNone},
{"opcode_61", 0, false, kAluOpChangedStateNone},
{"opcode_62", 0, false, kAluOpChangedStateNone},
{"opcode_63", 0, false, kAluOpChangedStateNone},
};
const AluVectorOpcodeInfo kAluVectorOpcodeInfos[32] = {
{"add", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"mul", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"max", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"min", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"seq", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"sgt", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"sge", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"sne", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"frc", {0b1111}, kAluOpChangedStateNone},
{"trunc", {0b1111}, kAluOpChangedStateNone},
{"floor", {0b1111}, kAluOpChangedStateNone},
{"mad", {0b1111, 0b1111, 0b1111}, kAluOpChangedStateNone},
{"cndeq", {0b1111, 0b1111, 0b1111}, kAluOpChangedStateNone},
{"cndge", {0b1111, 0b1111, 0b1111}, kAluOpChangedStateNone},
{"cndgt", {0b1111, 0b1111, 0b1111}, kAluOpChangedStateNone},
{"dp4", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"dp3", {0b0111, 0b0111}, kAluOpChangedStateNone},
{"dp2add", {0b0011, 0b0011, 0b0001}, kAluOpChangedStateNone},
{"cube", {0b1111, 0b1111}, kAluOpChangedStateNone},
{"max4", {0b1111}, kAluOpChangedStateNone},
{"setp_eq_push", {0b1001, 0b1001}, kAluOpChangedStatePredicate},
{"setp_ne_push", {0b1001, 0b1001}, kAluOpChangedStatePredicate},
{"setp_gt_push", {0b1001, 0b1001}, kAluOpChangedStatePredicate},
{"setp_ge_push", {0b1001, 0b1001}, kAluOpChangedStatePredicate},
{"kill_eq", {0b1111, 0b1111}, kAluOpChangedStatePixelKill},
{"kill_gt", {0b1111, 0b1111}, kAluOpChangedStatePixelKill},
{"kill_ge", {0b1111, 0b1111}, kAluOpChangedStatePixelKill},
{"kill_ne", {0b1111, 0b1111}, kAluOpChangedStatePixelKill},
{"dst", {0b0110, 0b1010}, kAluOpChangedStateNone},
{"maxa", {0b1111, 0b1111}, kAluOpChangedStateAddressRegister},
{"opcode_30", {}, kAluOpChangedStateNone},
{"opcode_31", {}, kAluOpChangedStateNone},
};
} // namespace ucode
} // namespace gpu
} // namespace xe
File diff suppressed because it is too large. Load diff
+44
View File
@@ -0,0 +1,44 @@
// SpeedBreaker: minimal stand-ins for the Xenia base utilities used by the
// Xenos headers ported from Xenia (BSD license, see headers in this folder).
#pragma once
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
#define static_assert_size(type, size) static_assert(sizeof(type) == (size), "bad size for " #type)
#define assert_true(x) assert(x)
#define assert_false(x) assert(!(x))
#define assert_zero(x) assert((x) == 0)
#define assert_not_zero(x) assert((x) != 0)
#define assert_always(...) assert(false)
#define assert_unhandled_case(x) assert(false && "unhandled case")
#define XE_FORCEINLINE inline __attribute__((always_inline))
namespace xe
{
template<typename T, size_t N>
constexpr size_t countof(T (&)[N]) { return N; }
inline uint16_t byte_swap(uint16_t v) { return __builtin_bswap16(v); }
inline uint32_t byte_swap(uint32_t v) { return __builtin_bswap32(v); }
inline uint64_t byte_swap(uint64_t v) { return __builtin_bswap64(v); }
// Hosts are little-endian (x86-64, arm64).
template<typename T>
using le = T;
typedef uint32_t fourcc_t;
constexpr fourcc_t make_fourcc(const char (&s)[5])
{
return (uint32_t(s[0]) << 24) | (uint32_t(s[1]) << 16) | (uint32_t(s[2]) << 8) | uint32_t(s[3]);
}
namespace memory
{
template<typename T, typename F>
T Reinterpret(F from) { T to; std::memcpy(&to, &from, sizeof(T)); return to; }
}
}
File diff suppressed because it is too large. Load diff
+240
View File
@@ -0,0 +1,240 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Occlusion query reports (EVENT_WRITE_ZPD), as this game's D3D reads them:
// the bookkeeping of gpu/renderer.cpp's counting (NFSMW_OCCLUSION=1) and the
// store order of an END report. Vulkan-free: tests/zpd_report_test.cpp
// checks it alone.
//
// D3D's Issue writes each query's reports into a 64-byte block per EDRAM
// tile: the BEGIN ZPD at block+0x20, the END ZPD at block+0x00 (the latched
// RB_SAMPLE_COUNT_ADDR). A report is 8 dwords, Total, ZFail, ZPass and
// StencilFail, each an A and a B lane, read little-endian (lwbrx). Issue
// BEGIN stores 0xFFFFFEED (big-endian) in the A lanes (+0/+8/+16/+24) of the
// LAST tile's END report; GetData is pending while all four still hold it,
// then returns the sum over tiles of END.ZPass(A+B) - BEGIN.ZPass(A+B). The
// game takes a count below 2^24 only. Every BEGIN report is all zeros, so an
// END report holds the samples of its own bracket: valid whenever it lands,
// even after the next BEGIN (a running counter would then go negative).
#pragma once
#include <array>
#include <atomic>
#include <cstdint>
namespace gpu::zpd
{
enum class Kind { Begin, End, Unknown };
// By the report's offset in its block.
inline Kind Classify(uint32_t address)
{
switch (address & 0x3F)
{
case 0x20: return Kind::Begin;
case 0x00: return Kind::End;
default: return Kind::Unknown;
}
}
// Guest samples for `raw` host samples: `num` guest samples per host
// pixel (the guest surface's MSAA; the host renders 1 sample), over
// `den` host pixels per guest pixel (the render scale, sx * sy). Rounded
// to nearest, at least 1 when anything passed (as Xenia does).
inline uint64_t Normalize(uint64_t raw, uint32_t num, uint32_t den)
{
if (raw == 0)
return 0;
if (den == 0)
den = 1;
uint64_t guest = (raw * num + den / 2) / den;
return guest ? guest : 1;
}
// An END's count from its segments' sum. A runaway one is rejected by
// the game, which keeps its value (as from the console), so it is
// saturated at 2^24, never cut to 32 bits: GetData keeps the low 32 bits
// of its sum over tiles, which no sum of up to 255 saturated tiles wraps
// into a count the game would take.
constexpr uint64_t kRunaway = 1u << 24;
inline uint32_t EndCount(uint64_t sum)
{
return uint32_t(sum < kRunaway ? sum : kRunaway);
}
// An END report of `count` samples, in host order (the guest reads it
// byte-swapped: no swap here). Total_A = ZPass_A = count, the rest 0.
inline std::array<uint32_t, 8> ComposeEnd(uint32_t count)
{
return { count, 0, 0, 0, count, 0, 0, 0 };
}
// The order of its stores: the B lanes (no marker there), then ZPass_A,
// which makes the report ready with its count in place, then the other
// A lanes; a release fence between the three. GetData's loads are
// unordered (no barrier in it), so a read racing them sees the marker in
// all four A lanes (pending), the marker in a lane it sums (0xEDFEFFFF
// read little-endian: >= 2^24, which the game rejects) or real counts:
// every B lane and BEGIN report is 0, so each tile adds its ZPass_A, this
// END's or (a stale load) the one before. Untiled only tile 0 counts:
// this END's count or the last. With NFSMW_TILING=1 one sum can mix
// tiles of adjacent frames (also when a late END lands after the next
// BEGIN's marker and makes that query ready). Indices into the 8
// dwords; phases of 4, 1 and 3.
constexpr uint8_t kEndOrder[8] = { 1, 3, 5, 7, 4, 0, 2, 6 };
constexpr uint8_t kEndPhaseEnd[3] = { 4, 5, 8 };
// `store(offset, value)` stores one dword of the report (offset in bytes;
// the callers open the write watch's guard once around all eight).
template<typename Store>
void StoreEnd(uint32_t count, Store&& store)
{
const std::array<uint32_t, 8> v = ComposeEnd(count);
uint32_t i = 0;
for (uint32_t phase = 0; phase < 3; phase++)
{
if (phase)
std::atomic_thread_fence(std::memory_order_release);
for (; i < kEndPhaseEnd[phase]; i++)
store(uint32_t(kEndOrder[i]) * 4, v[kEndOrder[i]]);
}
}
// A query bracket: BEGIN seen, END not yet. `s0` is the first counting
// segment that can belong to it (segments are numbered in recording
// order and never span a ZPD), `frame` when it was opened.
struct Bracket
{
uint32_t beginAddress = 0;
uint64_t s0 = 0, frame = 0;
};
// The open brackets, oldest first; the command processor's.
class BracketBook
{
public:
static constexpr uint32_t kMax = 8;
enum class Opened { New, Replaced, DroppedOldest };
// A BEGIN at `beginAddress`. One already open there (a query issued
// again before its END) is replaced; beyond kMax the oldest is
// dropped. `old`: the replaced or dropped bracket.
Opened Open(uint32_t beginAddress, uint64_t s0, uint64_t frame, Bracket* old = nullptr)
{
Opened result = Opened::New;
for (uint32_t i = 0; i < m_count; i++)
if (m_open[i].beginAddress == beginAddress)
{
if (old)
*old = m_open[i];
Erase(i);
result = Opened::Replaced;
break;
}
if (result == Opened::New && m_count == kMax)
{
if (old)
*old = m_open[0];
Erase(0);
result = Opened::DroppedOldest;
}
m_open[m_count++] = { beginAddress, s0, frame };
return result;
}
// The END at `endAddress` closes the bracket its BEGIN opened 0x20
// above it. False when none is open (its count is then 0).
bool Close(uint32_t endAddress, Bracket* closed = nullptr)
{
for (uint32_t i = 0; i < m_count; i++)
if (m_open[i].beginAddress == endAddress + 0x20)
{
if (closed)
*closed = m_open[i];
Erase(i);
return true;
}
return false;
}
// Drops the brackets opened before `frame`: how many; `first`, the
// oldest of them.
uint32_t DropBefore(uint64_t frame, Bracket* first = nullptr)
{
uint32_t dropped = 0;
for (uint32_t i = 0; i < m_count;)
{
if (m_open[i].frame >= frame)
{
i++;
continue;
}
if (first && dropped == 0)
*first = m_open[i];
dropped++;
Erase(i);
}
return dropped;
}
bool Empty() const { return m_count == 0; }
uint32_t Size() const { return m_count; }
const Bracket& operator[](uint32_t i) const { return m_open[i]; }
private:
void Erase(uint32_t i)
{
for (; i + 1 < m_count; i++)
m_open[i] = m_open[i + 1];
m_count--;
}
Bracket m_open[kMax];
uint32_t m_count = 0;
};
// Finished segments' counts by segment id, the last kSize of them (the
// completion thread's). Each entry carries its id: a bracket summing an
// id that was never stored, or was overwritten, counts it as lost.
// Ids start at 1 (an empty entry's id is 0).
class ResultRing
{
public:
static constexpr uint32_t kSize = 4096;
void Put(uint64_t id, uint64_t guest, uint64_t raw)
{
m_entries[id % kSize] = { id, guest, raw };
}
// Guest samples of segments [s0, s1); `lost` and `raw` (host
// samples) are added to.
uint64_t Sum(uint64_t s0, uint64_t s1, uint32_t& lost, uint64_t* raw = nullptr) const
{
uint64_t sum = 0;
if (s1 > s0 && s1 - s0 > kSize)
{
lost += uint32_t(s1 - s0 - kSize);
s0 = s1 - kSize;
}
for (uint64_t id = s0; id < s1; id++)
{
const Entry& e = m_entries[id % kSize];
if (e.id != id || id == 0)
{
lost++;
continue;
}
sum += e.guest;
if (raw)
*raw += e.raw;
}
return sum;
}
private:
struct Entry
{
uint64_t id = 0, guest = 0, raw = 0;
};
Entry m_entries[kSize];
};
}
+824
View File
@@ -0,0 +1,824 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See input.h.
#include <stdafx.h>
#include "input.h"
#include <cpu/guest_time.h>
#include <kernel/function.h>
#include <ui/ui.h>
#include <user/settings.h>
#include <SDL3/SDL.h>
namespace
{
constexpr uint32_t ERROR_SUCCESS = 0;
constexpr uint32_t ERROR_DEVICE_NOT_CONNECTED = 0x48F;
struct Pad
{
uint16_t buttons = 0;
uint8_t leftTrigger = 0, rightTrigger = 0;
int16_t lx = 0, ly = 0, rx = 0, ry = 0;
};
std::mutex s_mutex;
Pad s_pad; // latest sample (devices only)
bool s_deviceConnected = false; // a gamepad, or the keyboard of our window
uint32_t s_packet = 0;
uint16_t s_rumbleLow = 0, s_rumbleHigh = 0;
bool s_rumbleDirty = false;
SDL_Gamepad* s_gamepad = nullptr;
std::atomic<bool> s_blocked{ false };
// Main thread: buttons held when a menu closed, hidden from the game
// until each is released (the B, Start or A that closed it).
uint16_t s_latchedButtons = 0;
bool s_wasBlocked = false;
bool s_scriptSuppressed = false; // under s_mutex: no scripted input while a menu is open
// The controller's Back is held back from the game until it's clear it
// isn't the start of the Back + Start settings chord: released (then
// replayed as a short tap), or held 250 ms without Start. Main thread.
// In guest time, so a suspension can't eat the replayed tap.
enum class BackState { Idle, Pending, Passing, Replay, Swallow };
BackState s_backState = BackState::Idle;
int64_t s_backSince = 0; // guest ns
bool PadBackForGame(bool back, bool start)
{
int64_t now = guesttime::NowNs();
switch (s_backState)
{
case BackState::Idle:
if (back)
{
s_backState = start ? BackState::Swallow : BackState::Pending;
s_backSince = now;
}
return false;
case BackState::Pending:
if (start)
{
s_backState = BackState::Swallow; // the chord: the UI has it
return false;
}
if (!back)
{
s_backState = BackState::Replay;
s_backSince = now;
return true;
}
if (now - s_backSince >= 250'000'000)
{
s_backState = BackState::Passing;
return true;
}
return false;
case BackState::Passing:
if (!back)
s_backState = BackState::Idle;
return back;
case BackState::Replay:
if (now - s_backSince < 100'000'000)
return true;
s_backState = BackState::Idle;
return false;
case BackState::Swallow:
if (!back && !start)
s_backState = BackState::Idle;
return false;
}
return false;
}
// A scripted input: buttons (or analog: RT/LT triggers, LL/LR/LU/LD left
// stick) held over [time, time + duration).
struct ScriptTap { double time; uint16_t buttons; int16_t lx, ly; double duration = 0.15; uint8_t lt = 0, rt = 0; };
std::vector<ScriptTap> s_script;
// Guest ns at launch: the script and the virtual pad run on guest time,
// so a press due while the game is suspended comes after it instead of
// being lost.
const int64_t s_start = guesttime::NowNs();
double SecondsSinceLaunch()
{
return double(guesttime::NowNs() - s_start) / 1e9;
}
int16_t ToShort(float v)
{
return int16_t(std::clamp(v * 32767.0f, -32768.0f, 32767.0f));
}
// Radial deadzone with rescale: |v| < dz -> 0, else (|v|-dz)/(1-dz).
void ApplyDeadzone(float& x, float& y, float dz)
{
float m = std::sqrt(x * x + y * y);
if (m <= dz)
{
x = y = 0.0f;
return;
}
float scaled = std::min(1.0f, (m - dz) / (1.0f - dz));
x = x / m * scaled;
y = y / m * scaled;
}
uint8_t Trigger(float v, float dz)
{
if (v <= dz)
return 0;
return uint8_t(std::clamp((v - dz) / (1.0f - dz), 0.0f, 1.0f) * 255.0f + 0.5f);
}
uint16_t ButtonByName(const std::string& n)
{
static const std::pair<const char*, uint16_t> names[] = {
{ "UP", XAMINPUT_GAMEPAD_DPAD_UP }, { "DOWN", XAMINPUT_GAMEPAD_DPAD_DOWN },
{ "LEFT", XAMINPUT_GAMEPAD_DPAD_LEFT }, { "RIGHT", XAMINPUT_GAMEPAD_DPAD_RIGHT },
{ "START", XAMINPUT_GAMEPAD_START }, { "BACK", XAMINPUT_GAMEPAD_BACK },
{ "LS", XAMINPUT_GAMEPAD_LEFT_THUMB }, { "RS", XAMINPUT_GAMEPAD_RIGHT_THUMB },
{ "LB", XAMINPUT_GAMEPAD_LEFT_SHOULDER }, { "RB", XAMINPUT_GAMEPAD_RIGHT_SHOULDER },
{ "A", XAMINPUT_GAMEPAD_A }, { "B", XAMINPUT_GAMEPAD_B }, { "X", XAMINPUT_GAMEPAD_X }, { "Y", XAMINPUT_GAMEPAD_Y },
};
for (auto& [name, bit] : names)
if (n == name)
return bit;
return 0;
}
void ParseScript()
{
const char* s = std::getenv("NFSMW_INPUT_SCRIPT");
if (!s)
return;
std::string all = s;
size_t pos = 0;
while (pos < all.size())
{
size_t end = all.find(',', pos);
std::string item = all.substr(pos, end == std::string::npos ? std::string::npos : end - pos);
pos = end == std::string::npos ? all.size() : end + 1;
size_t colon = item.find(':');
if (colon == std::string::npos)
continue;
// "sec:BUTTON", "from-to/step:BUTTON" (a tap every `step` s) or
// "from~to:BUTTON" (held).
std::string when = item.substr(0, colon);
std::string name = item.substr(colon + 1);
ScriptTap proto{ 0, ButtonByName(name), 0, 0 };
if (name == "RT") proto.rt = 255;
else if (name == "LT") proto.lt = 255;
else if (name == "LL") proto.lx = -32767;
else if (name == "LR") proto.lx = 32767;
else if (name == "LU") proto.ly = 32767;
else if (name == "LD") proto.ly = -32767;
else if (proto.buttons == 0)
{
fprintf(stderr, "[input] script: unknown button in \"%s\"\n", item.c_str());
continue;
}
uint16_t button = proto.buttons;
if (size_t tilde = when.find('~'); tilde != std::string::npos)
{
ScriptTap hold = proto;
hold.time = std::atof(when.substr(0, tilde).c_str());
hold.duration = std::atof(when.substr(tilde + 1).c_str()) - hold.time;
s_script.push_back(hold);
continue;
}
size_t dash = when.find('-'), slash = when.find('/');
if (dash != std::string::npos && slash != std::string::npos && slash > dash)
{
double from = std::atof(when.substr(0, dash).c_str());
double to = std::atof(when.substr(dash + 1, slash - dash - 1).c_str());
double step = std::max(0.2, std::atof(when.substr(slash + 1).c_str()));
for (double t = from; t <= to; t += step)
{
ScriptTap tap = proto;
tap.time = t;
s_script.push_back(tap);
}
}
else
{
ScriptTap tap = proto;
tap.time = std::atof(when.c_str());
s_script.push_back(tap);
}
}
fprintf(stderr, "[input] script: %zu taps\n", s_script.size());
}
// Apply the scripted inputs active now on top of the device state.
void ApplyScript(Pad& p)
{
if (s_script.empty())
return;
double t = SecondsSinceLaunch();
for (const auto& tap : s_script)
if (t >= tap.time && t < tap.time + tap.duration)
{
p.buttons |= tap.buttons;
p.leftTrigger = std::max(p.leftTrigger, tap.lt);
p.rightTrigger = std::max(p.rightTrigger, tap.rt);
if (tap.lx) p.lx = tap.lx;
if (tap.ly) p.ly = tap.ly;
}
}
const bool s_scriptParsed = (ParseScript(), true);
// NFSMW_VIRTUAL_PAD="2:LX=0.05,4:LX=0.2,6:RT=0.01": an SDL virtual
// controller, the only one opened, whose axes move to those values at
// seconds since launch (sticks LX/LY/RX/RY -1..1, up positive; triggers
// LT/RT 0..1) and hold them. NFSMW_INPUT_SCRIPT feeds the game after the
// device path; this goes through it, so the deadzones and Vibration
// reach it as they would a real controller, for tests without one. Each
// step logs what the game then gets; the rumble the game asks for and
// the rumble the pad receives are logged.
struct PadStep { double time; SDL_GamepadAxis axis; float value; std::string text; };
std::vector<PadStep> s_padSteps;
size_t s_padNext = 0;
SDL_JoystickID s_virtualPadId = 0;
SDL_Joystick* s_virtualPad = nullptr;
bool SDLCALL VirtualPadRumble(void*, Uint16 low, Uint16 high)
{
fprintf(stderr, "[input] virtual pad: rumble low %u, high %u\n", unsigned(low), unsigned(high));
return true;
}
void AttachVirtualPad()
{
const char* v = std::getenv("NFSMW_VIRTUAL_PAD");
if (!v || !*v)
return;
// A test's window rarely has the keyboard focus (on the Mac it's
// started from a shell behind other windows), and without it SDL
// drops stick and trigger changes: the pad kept its first value.
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
static const std::pair<const char*, SDL_GamepadAxis> axes[] = {
{ "LX", SDL_GAMEPAD_AXIS_LEFTX }, { "LY", SDL_GAMEPAD_AXIS_LEFTY }, { "RX", SDL_GAMEPAD_AXIS_RIGHTX },
{ "RY", SDL_GAMEPAD_AXIS_RIGHTY }, { "LT", SDL_GAMEPAD_AXIS_LEFT_TRIGGER }, { "RT", SDL_GAMEPAD_AXIS_RIGHT_TRIGGER },
};
for (std::string_view rest = v; !rest.empty();)
{
size_t comma = rest.find(',');
std::string item(rest.substr(0, comma));
rest = comma == std::string_view::npos ? std::string_view() : rest.substr(comma + 1);
size_t colon = item.find(':'), equals = item.find('=');
if (colon == std::string::npos || equals == std::string::npos || equals < colon)
continue;
std::string name = item.substr(colon + 1, equals - colon - 1);
bool found = false;
for (auto& [n, axis] : axes)
if (name == n)
{
s_padSteps.push_back({ std::atof(item.c_str()), axis, float(std::atof(item.c_str() + equals + 1)), item.substr(colon + 1) });
found = true;
}
if (!found)
fprintf(stderr, "[input] NFSMW_VIRTUAL_PAD: unknown axis in \"%s\"\n", item.c_str());
}
std::stable_sort(s_padSteps.begin(), s_padSteps.end(), [](const PadStep& a, const PadStep& b) { return a.time < b.time; });
SDL_VirtualJoystickDesc desc;
SDL_INIT_INTERFACE(&desc);
desc.type = SDL_JOYSTICK_TYPE_GAMEPAD;
desc.naxes = SDL_GAMEPAD_AXIS_COUNT;
desc.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
desc.name = "SpeedBreaker virtual pad";
desc.Rumble = VirtualPadRumble;
s_virtualPadId = SDL_AttachVirtualJoystick(&desc);
s_virtualPad = s_virtualPadId ? SDL_OpenJoystick(s_virtualPadId) : nullptr;
if (!s_virtualPad)
{
fprintf(stderr, "[input] NFSMW_VIRTUAL_PAD: no virtual controller: %s\n", SDL_GetError());
return;
}
fprintf(stderr, "[input] virtual pad attached, %zu steps\n", s_padSteps.size());
}
// Moves the virtual pad's axes due by now. Returns the steps taken ("" if none).
std::string StepVirtualPad()
{
if (!s_virtualPad || s_padNext >= s_padSteps.size())
return {};
double t = SecondsSinceLaunch();
std::string taken;
for (; s_padNext < s_padSteps.size() && s_padSteps[s_padNext].time <= t; s_padNext++)
{
const PadStep& step = s_padSteps[s_padNext];
float value = step.value;
bool trigger = step.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER || step.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER;
if (step.axis == SDL_GAMEPAD_AXIS_LEFTY || step.axis == SDL_GAMEPAD_AXIS_RIGHTY)
value = -value; // SDL: down is positive
// A virtual gamepad's triggers span the whole axis (-32768 is released).
float raw = trigger ? std::clamp(value, 0.0f, 1.0f) * 65535.0f - 32768.0f : std::clamp(value, -1.0f, 1.0f) * 32767.0f;
SDL_SetJoystickVirtualAxis(s_virtualPad, int(step.axis), Sint16(std::lround(std::clamp(raw, -32768.0f, 32767.0f))));
taken += (taken.empty() ? "" : " ") + step.text;
}
if (!taken.empty())
SDL_UpdateJoysticks(); // read back this frame
return taken;
}
void OpenFirstGamepad()
{
if (s_gamepad)
return;
if (s_virtualPad)
{
// Only the test's pad: a real controller nearby must not rumble.
s_gamepad = SDL_OpenGamepad(s_virtualPadId);
if (s_gamepad)
fprintf(stderr, "[input] gamepad: %s\n", SDL_GetGamepadName(s_gamepad));
return;
}
int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count);
if (ids && count > 0)
{
s_gamepad = SDL_OpenGamepad(ids[0]);
if (s_gamepad)
{
// What it can do: a pad without motors takes rumble and ignores it.
SDL_PropertiesID props = SDL_GetGamepadProperties(s_gamepad);
fprintf(stderr, "[input] gamepad: %s (rumble %s, trigger rumble %s)\n", SDL_GetGamepadName(s_gamepad),
SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RUMBLE_BOOLEAN, false) ? "yes" : "no",
SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_TRIGGER_RUMBLE_BOOLEAN, false) ? "yes" : "no");
}
}
SDL_free(ids);
}
#if defined(__APPLE__) && TARGET_OS_IOS
// A phone or tablet has nothing to play the game with but a controller
// (or a keyboard attached to it, which counts as on desktop), and a
// player who has just installed it may not know. So a notice when there
// is none: once as the game starts (not over the installer, which a
// finger works), and again when the last one goes. Each waits a moment
// and looks again, so a controller that iOS reports a little late (a
// Bluetooth pad woken by its button after launch takes a few seconds),
// or one that drops out and straight back, brings up nothing; and one
// that turns up while the notice shows takes it down. Main thread;
// guest ns, 0: none due.
constexpr const char* kNeedsController = "The game needs a controller: an Xbox or PlayStation one over Bluetooth, or a Backbone.";
constexpr const char* kControllerGone = "Controller disconnected: the game needs one (Xbox or PlayStation over Bluetooth, or a Backbone).";
int64_t s_noControllerAt = 0;
const char* s_noControllerText = nullptr;
void NoControllerNoticeIn(int64_t ns, const char* text)
{
s_noControllerAt = guesttime::NowNs() + ns;
s_noControllerText = text;
}
void NoControllerNotice()
{
if (!s_noControllerAt || guesttime::NowNs() < s_noControllerAt)
return;
s_noControllerAt = 0;
// (NFSMW_INPUT_SCRIPT is the controller of an automated run.)
if (s_gamepad || SDL_HasKeyboard() || !s_script.empty())
return;
fprintf(stderr, "[input] no controller: %s\n", s_noControllerText);
ui::Toast(s_noControllerText, 10.0);
}
// Something to play with turned up: a notice still showing (or waiting
// its turn) no longer holds.
void WithdrawNoControllerNotice()
{
ui::WithdrawToast(kNeedsController);
ui::WithdrawToast(kControllerGone);
}
#endif
}
namespace
{
const bool s_inputLog = std::getenv("NFSMW_INPUT_LOG") != nullptr;
// NFSMW_INPUT_LOG=1: once a second, the left stick as the device reports
// it (last value and range over the second) and as the game receives it.
// A stick at rest must reach the game as exactly 0 (the drift check).
// The triggers too (a worn one that never reads 0 holds the brake).
void LogStick(float rawX, float rawY, float x, float y, float rawLt, float rawRt, uint8_t lt, uint8_t rt)
{
if (!s_inputLog)
return;
static float minX = 1, maxX = -1, minY = 1, maxY = -1;
static auto last = std::chrono::steady_clock::now();
minX = std::min(minX, rawX); maxX = std::max(maxX, rawX);
minY = std::min(minY, rawY); maxY = std::max(maxY, rawY);
auto now = std::chrono::steady_clock::now();
if (now - last < std::chrono::seconds(1))
return;
fprintf(stderr, "[input] left stick raw (%+.4f, %+.4f) range x[%+.4f, %+.4f] y[%+.4f, %+.4f] -> game (%d, %d); "
"triggers raw (%.3f, %.3f) -> game (%u, %u)\n",
rawX, rawY, minX, maxX, minY, maxY, int(ToShort(x)), int(ToShort(y)), rawLt, rawRt, unsigned(lt), unsigned(rt));
minX = minY = 1;
maxX = maxY = -1;
last = now;
}
}
namespace hid
{
void Initialize()
{
if (!SDL_InitSubSystem(SDL_INIT_GAMEPAD))
fprintf(stderr, "[input] SDL gamepad init failed: %s\n", SDL_GetError());
fprintf(stderr, "[input] stick deadzone %.2f, trigger deadzone %.2f\n", settings::GetFloat(settings::Id::StickDeadzone),
settings::GetFloat(settings::Id::TriggerDeadzone));
AttachVirtualPad();
OpenFirstGamepad();
std::lock_guard lock(s_mutex);
s_deviceConnected = true; // the keyboard always counts as controller 0
}
void NoteGameStarting()
{
#if defined(__APPLE__) && TARGET_OS_IOS
NoControllerNoticeIn(5'000'000'000, kNeedsController);
#endif
}
void HandleEvent(const SDL_Event& e)
{
if (e.type == SDL_EVENT_GAMEPAD_ADDED)
{
OpenFirstGamepad();
#if defined(__APPLE__) && TARGET_OS_IOS
if (s_gamepad)
WithdrawNoControllerNotice();
#endif
}
else if (e.type == SDL_EVENT_GAMEPAD_REMOVED && s_gamepad && e.gdevice.which == SDL_GetGamepadID(s_gamepad))
{
SDL_CloseGamepad(s_gamepad);
s_gamepad = nullptr;
fprintf(stderr, "[input] gamepad removed\n");
OpenFirstGamepad();
#if defined(__APPLE__) && TARGET_OS_IOS
if (!s_gamepad)
NoControllerNoticeIn(1'000'000'000, kControllerGone);
#endif
}
#if defined(__APPLE__) && TARGET_OS_IOS
else if (e.type == SDL_EVENT_KEYBOARD_ADDED)
WithdrawNoControllerNotice(); // (a keyboard counts as a controller here)
#endif
}
void Shutdown()
{
if (s_gamepad)
{
SDL_RumbleGamepad(s_gamepad, 0, 0, 0);
SDL_CloseGamepad(s_gamepad);
s_gamepad = nullptr;
}
}
void Suspend()
{
// Update sends a rumble for 5 s at a time and doesn't run meanwhile:
// stop it now rather than buzz on for a game that stands still.
if (s_gamepad)
SDL_RumbleGamepad(s_gamepad, 0, 0, 0);
}
void Resume()
{
// The next Update sends the game's last request again, as the
// Vibration setting allows.
std::lock_guard lock(s_mutex);
s_rumbleDirty = true;
}
void SetBlocked(bool blocked)
{
s_blocked.store(blocked, std::memory_order_relaxed);
}
void Update()
{
#if defined(__APPLE__) && TARGET_OS_IOS
NoControllerNotice();
#endif
Pad p;
float lx = 0, ly = 0, rx = 0, ry = 0, lt = 0, rt = 0;
bool padBack = false; // the controller's Back, as the device reports it
float rawX = 0, rawY = 0; // the left stick before its deadzone (NFSMW_INPUT_LOG)
const std::string padSteps = StepVirtualPad();
const float deadzone = settings::GetFloat(settings::Id::StickDeadzone);
const float triggerDeadzone = settings::GetFloat(settings::Id::TriggerDeadzone);
if (s_gamepad)
{
static const std::pair<SDL_GamepadButton, uint16_t> map[] = {
{ SDL_GAMEPAD_BUTTON_SOUTH, XAMINPUT_GAMEPAD_A }, { SDL_GAMEPAD_BUTTON_EAST, XAMINPUT_GAMEPAD_B },
{ SDL_GAMEPAD_BUTTON_WEST, XAMINPUT_GAMEPAD_X }, { SDL_GAMEPAD_BUTTON_NORTH, XAMINPUT_GAMEPAD_Y },
{ SDL_GAMEPAD_BUTTON_BACK, XAMINPUT_GAMEPAD_BACK }, { SDL_GAMEPAD_BUTTON_START, XAMINPUT_GAMEPAD_START },
{ SDL_GAMEPAD_BUTTON_LEFT_STICK, XAMINPUT_GAMEPAD_LEFT_THUMB },
{ SDL_GAMEPAD_BUTTON_RIGHT_STICK, XAMINPUT_GAMEPAD_RIGHT_THUMB },
{ SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, XAMINPUT_GAMEPAD_LEFT_SHOULDER },
{ SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, XAMINPUT_GAMEPAD_RIGHT_SHOULDER },
{ SDL_GAMEPAD_BUTTON_DPAD_UP, XAMINPUT_GAMEPAD_DPAD_UP }, { SDL_GAMEPAD_BUTTON_DPAD_DOWN, XAMINPUT_GAMEPAD_DPAD_DOWN },
{ SDL_GAMEPAD_BUTTON_DPAD_LEFT, XAMINPUT_GAMEPAD_DPAD_LEFT }, { SDL_GAMEPAD_BUTTON_DPAD_RIGHT, XAMINPUT_GAMEPAD_DPAD_RIGHT },
};
for (auto& [button, bit] : map)
if (bit != XAMINPUT_GAMEPAD_BACK && SDL_GetGamepadButton(s_gamepad, button))
p.buttons |= bit;
padBack = SDL_GetGamepadButton(s_gamepad, SDL_GAMEPAD_BUTTON_BACK);
if (PadBackForGame(padBack, SDL_GetGamepadButton(s_gamepad, SDL_GAMEPAD_BUTTON_START)))
p.buttons |= XAMINPUT_GAMEPAD_BACK;
auto axis = [](SDL_Gamepad* g, SDL_GamepadAxis a) { return float(SDL_GetGamepadAxis(g, a)) / 32767.0f; };
lx = axis(s_gamepad, SDL_GAMEPAD_AXIS_LEFTX);
ly = -axis(s_gamepad, SDL_GAMEPAD_AXIS_LEFTY); // XInput: up is positive
rx = axis(s_gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
ry = -axis(s_gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
lt = axis(s_gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER);
rt = axis(s_gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER);
rawX = lx;
rawY = ly;
ApplyDeadzone(lx, ly, deadzone);
ApplyDeadzone(rx, ry, deadzone);
}
const float rawLt = lt, rawRt = rt;
// Keyboard (digital; overrides the analog values it touches).
const bool* k = SDL_GetKeyboardState(nullptr);
if (k)
{
static const std::pair<SDL_Scancode, uint16_t> keys[] = {
{ SDL_SCANCODE_UP, XAMINPUT_GAMEPAD_DPAD_UP }, { SDL_SCANCODE_DOWN, XAMINPUT_GAMEPAD_DPAD_DOWN },
{ SDL_SCANCODE_LEFT, XAMINPUT_GAMEPAD_DPAD_LEFT }, { SDL_SCANCODE_RIGHT, XAMINPUT_GAMEPAD_DPAD_RIGHT },
{ SDL_SCANCODE_SPACE, XAMINPUT_GAMEPAD_A }, { SDL_SCANCODE_BACKSPACE, XAMINPUT_GAMEPAD_B },
{ SDL_SCANCODE_LSHIFT, XAMINPUT_GAMEPAD_X }, { SDL_SCANCODE_LCTRL, XAMINPUT_GAMEPAD_Y },
{ SDL_SCANCODE_Q, XAMINPUT_GAMEPAD_LEFT_SHOULDER }, { SDL_SCANCODE_E, XAMINPUT_GAMEPAD_RIGHT_SHOULDER },
{ SDL_SCANCODE_RETURN, XAMINPUT_GAMEPAD_START }, { SDL_SCANCODE_ESCAPE, XAMINPUT_GAMEPAD_BACK },
{ SDL_SCANCODE_C, XAMINPUT_GAMEPAD_RIGHT_THUMB },
};
for (auto& [key, bit] : keys)
if (k[key])
p.buttons |= bit;
float kx = float(k[SDL_SCANCODE_RIGHT] || k[SDL_SCANCODE_D]) - float(k[SDL_SCANCODE_LEFT] || k[SDL_SCANCODE_A]);
float ky = float(k[SDL_SCANCODE_UP]) - float(k[SDL_SCANCODE_DOWN]);
if (kx != 0.0f)
lx = kx;
if (ky != 0.0f)
ly = ky;
if (k[SDL_SCANCODE_W])
rt = 1.0f;
if (k[SDL_SCANCODE_S])
lt = 1.0f;
}
p.lx = ToShort(lx);
p.ly = ToShort(ly);
p.rx = ToShort(rx);
p.ry = ToShort(ry);
p.leftTrigger = Trigger(lt, triggerDeadzone);
p.rightTrigger = Trigger(rt, triggerDeadzone);
if (s_gamepad)
LogStick(rawX, rawY, lx, ly, rawLt, rawRt, p.leftTrigger, p.rightTrigger);
if (!padSteps.empty())
fprintf(stderr, "[input] virtual pad %s: left stick raw (%+.4f, %+.4f) -> game (%d, %d), right (%d, %d); "
"triggers raw (%.3f, %.3f) -> game (%u, %u); deadzones: stick %.2f, trigger %.2f%s\n",
padSteps.c_str(), rawX, rawY, int(p.lx), int(p.ly), int(p.rx), int(p.ry), rawLt, rawRt,
unsigned(p.leftTrigger), unsigned(p.rightTrigger), deadzone, triggerDeadzone,
s_blocked.load(std::memory_order_relaxed) ? " (a menu is open: the game sees an idle pad)" : "");
// While a menu has the input the game sees an idle pad; after it
// closes, each button still held stays hidden until released, so the
// press that closed the menu (B, Start, Esc) doesn't reach the game.
bool blocked = s_blocked.load(std::memory_order_relaxed);
if (s_wasBlocked && !blocked)
s_latchedButtons = p.buttons;
// A Back pressed in the menu (or the one that closed it) is never replayed.
if ((blocked || s_wasBlocked) && padBack)
s_backState = BackState::Swallow;
s_wasBlocked = blocked;
s_latchedButtons &= p.buttons;
if (blocked)
p = Pad{};
else
p.buttons &= ~s_latchedButtons;
uint16_t low, high;
bool rumble;
{
std::lock_guard lock(s_mutex);
s_pad = p;
s_scriptSuppressed = blocked;
rumble = s_rumbleDirty;
s_rumbleDirty = false;
low = s_rumbleLow;
high = s_rumbleHigh;
}
static bool vibration = true;
if (bool on = settings::GetBool(settings::Id::Vibration); on != vibration)
{
vibration = on;
rumble = true; // stop (or restart) a rumble under way
}
if (rumble && s_virtualPad)
{
// What the game asked for and what the controller gets, on
// change. Only with the test's pad: while driving the game
// changes its request every frame, which would be a line a frame
// in a player's last-run.log (speedbreaker.sh sets NFSMW_INPUT_LOG).
static uint64_t logged = ~0ull;
uint64_t now = uint64_t(low) << 17 | uint64_t(high) << 1 | (vibration ? 1u : 0u);
if (now != logged)
{
logged = now;
fprintf(stderr, "[input] rumble: game low %u, high %u; Vibration %s%s\n", unsigned(low), unsigned(high),
vibration ? "on" : "off (the controller gets 0)", s_gamepad ? "" : "; no controller");
}
}
if (!vibration)
low = high = 0;
if (rumble && s_gamepad)
SDL_RumbleGamepad(s_gamepad, low, high, low || high ? 5000 : 0);
}
}
namespace
{
// The pad as the game sees it: devices plus scripted taps. Caller holds s_mutex.
Pad CurrentPadLocked()
{
Pad p = s_pad;
if (!s_scriptSuppressed)
ApplyScript(p);
return p;
}
// Keystrokes (XamInputGetKeystroke[Ex]): edges of buttons, triggers and
// stick directions as XInput VK_PAD_* codes, with auto-repeat for the
// navigation keys, generated on demand from the same state as GetState.
struct KeyDef { uint16_t vk; bool repeats; bool (*down)(const Pad&); };
const KeyDef kKeys[] = {
{ 0x5800, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_A) != 0; } },
{ 0x5801, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_B) != 0; } },
{ 0x5802, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_X) != 0; } },
{ 0x5803, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_Y) != 0; } },
{ 0x5804, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_RIGHT_SHOULDER) != 0; } },
{ 0x5805, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_LEFT_SHOULDER) != 0; } },
{ 0x5806, false, [](const Pad& p) { return p.leftTrigger > 30; } },
{ 0x5807, false, [](const Pad& p) { return p.rightTrigger > 30; } },
{ 0x5810, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_UP) != 0; } },
{ 0x5811, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_DOWN) != 0; } },
{ 0x5812, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_LEFT) != 0; } },
{ 0x5813, true, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_DPAD_RIGHT) != 0; } },
{ 0x5814, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_START) != 0; } },
{ 0x5815, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_BACK) != 0; } },
{ 0x5816, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_LEFT_THUMB) != 0; } },
{ 0x5817, false, [](const Pad& p) { return (p.buttons & XAMINPUT_GAMEPAD_RIGHT_THUMB) != 0; } },
{ 0x5820, true, [](const Pad& p) { return p.ly > 16384; } },
{ 0x5821, true, [](const Pad& p) { return p.ly < -16384; } },
{ 0x5822, true, [](const Pad& p) { return p.lx > 16384; } },
{ 0x5823, true, [](const Pad& p) { return p.lx < -16384; } },
{ 0x5830, true, [](const Pad& p) { return p.ry > 16384; } },
{ 0x5831, true, [](const Pad& p) { return p.ry < -16384; } },
{ 0x5832, true, [](const Pad& p) { return p.rx > 16384; } },
{ 0x5833, true, [](const Pad& p) { return p.rx < -16384; } },
};
constexpr size_t kKeyCount = sizeof(kKeys) / sizeof(kKeys[0]);
bool s_keyDown[kKeyCount] = {};
// Guest ns: a key held through a suspension doesn't repeat as soon as it's back.
int64_t s_keyRepeatAt[kKeyCount] = {};
struct Keystroke
{
be<uint16_t> virtualKey;
be<uint16_t> unicode;
be<uint16_t> flags; // 1 key down, 2 key up, 4 repeat
uint8_t userIndex;
uint8_t hidCode;
};
static_assert(sizeof(Keystroke) == 8);
constexpr uint32_t ERROR_EMPTY = 0x10D2;
uint32_t NextKeystroke(Keystroke* out)
{
std::lock_guard lock(s_mutex);
if (!(s_deviceConnected || !s_script.empty()))
return ERROR_DEVICE_NOT_CONNECTED;
Pad p = CurrentPadLocked();
int64_t now = guesttime::NowNs();
auto emit = [&](size_t i, uint16_t flags) {
out->virtualKey = kKeys[i].vk;
out->unicode = 0;
out->flags = flags;
out->userIndex = 0;
out->hidCode = 0;
return ERROR_SUCCESS;
};
for (size_t i = 0; i < kKeyCount; i++)
{
bool down = kKeys[i].down(p);
if (down != s_keyDown[i])
{
s_keyDown[i] = down;
s_keyRepeatAt[i] = now + 400'000'000;
return emit(i, down ? 1 : 2);
}
}
for (size_t i = 0; i < kKeyCount; i++)
if (s_keyDown[i] && kKeys[i].repeats && now >= s_keyRepeatAt[i])
{
s_keyRepeatAt[i] = now + 100'000'000;
return emit(i, 1 | 4);
}
return ERROR_EMPTY;
}
}
uint32_t XamInputGetCapabilities(uint32_t userIndex, uint32_t flags, XAMINPUT_CAPABILITIES* caps)
{
bool scripted = !s_script.empty();
{
std::lock_guard lock(s_mutex);
if ((userIndex != 0 && userIndex != 0xFF) || !(s_deviceConnected || scripted))
return ERROR_DEVICE_NOT_CONNECTED;
}
memset(caps, 0, sizeof(*caps));
caps->Type = XAMINPUT_DEVTYPE_GAMEPAD;
caps->SubType = XAMINPUT_DEVSUBTYPE_GAMEPAD;
caps->Gamepad.wButtons = ByteSwap(uint16_t(0xF3FF));
caps->Gamepad.bLeftTrigger = 0xFF;
caps->Gamepad.bRightTrigger = 0xFF;
for (int16_t* v : { &caps->Gamepad.sThumbLX, &caps->Gamepad.sThumbLY, &caps->Gamepad.sThumbRX, &caps->Gamepad.sThumbRY })
*v = ByteSwap(int16_t(0xFFC0));
caps->Vibration.wLeftMotorSpeed = 0xFFFF;
caps->Vibration.wRightMotorSpeed = 0xFFFF;
return ERROR_SUCCESS;
}
uint32_t XamInputGetState(uint32_t userIndex, uint32_t flags, XAMINPUT_STATE* state)
{
Pad p;
uint32_t packet;
{
std::lock_guard lock(s_mutex);
if ((userIndex != 0 && userIndex != 0xFF) || !(s_deviceConnected || !s_script.empty()))
return ERROR_DEVICE_NOT_CONNECTED;
p = CurrentPadLocked();
static Pad last;
if (memcmp(&p, &last, sizeof(p)) != 0)
{
s_packet++;
last = p;
}
packet = s_packet;
}
state->dwPacketNumber = ByteSwap(packet);
state->Gamepad.wButtons = ByteSwap(p.buttons);
state->Gamepad.bLeftTrigger = p.leftTrigger;
state->Gamepad.bRightTrigger = p.rightTrigger;
state->Gamepad.sThumbLX = ByteSwap(p.lx);
state->Gamepad.sThumbLY = ByteSwap(p.ly);
state->Gamepad.sThumbRX = ByteSwap(p.rx);
state->Gamepad.sThumbRY = ByteSwap(p.ry);
return ERROR_SUCCESS;
}
uint32_t XamInputSetState(uint32_t userIndex, uint32_t flags, XAMINPUT_VIBRATION* vibration)
{
std::lock_guard lock(s_mutex);
if ((userIndex != 0 && userIndex != 0xFF) || !(s_deviceConnected || !s_script.empty()))
return ERROR_DEVICE_NOT_CONNECTED;
s_rumbleLow = ByteSwap(vibration->wLeftMotorSpeed);
s_rumbleHigh = ByteSwap(vibration->wRightMotorSpeed);
s_rumbleDirty = true;
return ERROR_SUCCESS;
}
// Xenia's semantics: user 0xFF ("any") is pinned to user 0; queries for
// non-gamepad devices report none connected.
uint32_t XamInputGetKeystroke(uint32_t userIndex, uint32_t flags, Keystroke* keystroke)
{
if (!keystroke)
return 0xA0; // ERROR_BAD_ARGUMENTS
if ((flags & 0xFF) && (flags & 1) == 0)
return ERROR_DEVICE_NOT_CONNECTED;
if ((userIndex & 0xFF) != 0xFF && userIndex != 0 && !(flags & 0x40000000))
return ERROR_DEVICE_NOT_CONNECTED;
return NextKeystroke(keystroke);
}
uint32_t XamInputGetKeystrokeEx(be<uint32_t>* userIndex, uint32_t flags, Keystroke* keystroke)
{
uint32_t result = XamInputGetKeystroke(userIndex ? uint32_t(*userIndex) : 0xFF, flags, keystroke);
if (result == ERROR_SUCCESS && userIndex)
*userIndex = 0;
return result;
}
GUEST_FUNCTION_HOOK(__imp__XamInputGetCapabilities, XamInputGetCapabilities);
GUEST_FUNCTION_HOOK(__imp__XamInputGetState, XamInputGetState);
GUEST_FUNCTION_HOOK(__imp__XamInputSetState, XamInputSetState);
GUEST_FUNCTION_HOOK(__imp__XamInputGetKeystrokeEx, XamInputGetKeystrokeEx);
+54
View File
@@ -0,0 +1,54 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Controller input (Phase 6): SDL3 gamepads and the keyboard, presented to
// the game as XInput controller 0 through XamInputGetState.
//
// - Sticks get a radial deadzone with rescaling (Settings > Input, or
// NFSMW_DEADZONE; default 0.08 of full deflection), so worn or drifting
// sticks rest at exactly zero while small movements still ramp up
// smoothly from the edge of the deadzone. Triggers get their own
// (NFSMW_TRIGGER_DEADZONE, default 0.02).
// - Keyboard: arrows = D-pad + left stick, W/S = gas/brake (RT/LT), A/D =
// steer, Space = A, Backspace = B, Left Shift = X, Left Ctrl = Y,
// Q/E = LB/RB, Enter = START, Esc = BACK, C = right stick click.
// - NFSMW_INPUT_SCRIPT="12:START,15.5:A,30-90/4:DOWN,100~160:RT": taps
// (150 ms) at seconds since launch, taps every `step` seconds over a
// range, or inputs held over a range; RT/LT (triggers) and LL/LR/LU/LD
// (left stick) besides the buttons. For automated runs; works headless.
// It feeds the game after the deadzones.
// - NFSMW_VIRTUAL_PAD="2:LX=0.05,4:RT=0.06": an SDL virtual controller whose
// axes take those values at those seconds, through the deadzones, and
// whose rumble is logged with what the game asked for (tests of the
// Input settings without a person).
// Both count seconds of guest time (cpu/guest_time.h): a suspension
// doesn't count, so what falls due during one happens after it.
// - NFSMW_INPUT_LOG=1: the left stick and triggers once a second, raw and
// as the game gets them.
// - iOS: with no controller (and no keyboard attached) a toast says the
// game needs one, 5 s after the game starts (NoteGameStarting) and 1 s
// after the last one goes; one connecting takes it down.
#pragma once
#include <SDL3/SDL_events.h>
namespace hid
{
// Main thread, after SDL video is up.
void Initialize();
// Main thread, as the game itself starts (after the installer, if it
// ran): on iOS, the "needs a controller" notice is due 5 s later if
// there is none by then. Elsewhere nothing.
void NoteGameStarting();
void HandleEvent(const SDL_Event& event);
// Main thread, once per presented frame: sample devices.
void Update();
// While set (a menu is open), the game sees an idle controller, and
// after it is cleared until every control has been released.
void SetBlocked(bool blocked);
// At exit: stop any rumble (the process may outlive nothing that would).
void Shutdown();
// Main thread, from the frame loop (not UIKit's callback), once the game
// is suspended (iOS: the app isn't active) and once it's back: Suspend
// stops any rumble, Resume sends the game's last request again.
void Suspend();
void Resume();
}
+720
View File
@@ -0,0 +1,720 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See disc_source.h.
//
// The XDVDFS walk (partition offsets, volume descriptor, the per-directory
// binary tree) is adapted from Unleashed Recompiled
// (https://github.com/hedge-dev/UnleashedRecomp), install/iso_file_system.cpp,
// GPL-3.0-or-later, which references Xenia's src/xenia/vfs/devices/disc_image_device.cc:
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2023 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// Modified for SpeedBreaker: pread in chunks instead of mapping the image;
// every offset, size and name is bounds-checked; loops, repeated nodes and
// runaway trees (in count or in total table size) are rejected instead of
// followed; a truncated image is reported as such (the usual way a 7.8 GB
// copy goes wrong).
#include "disc_source.h"
#include "manifest.h"
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <format>
#include <set>
#include <unordered_set>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
namespace install
{
namespace
{
namespace fs = std::filesystem;
static_assert(sizeof(off_t) == 8, "large file offsets are required");
constexpr uint64_t kSectorSize = 2048;
// Where the game partition starts: plain XISO (0), older rips
// (0xFB20, 0x20600), XGD3 (0x2080000) and XGD2 (0xFD90000). The
// volume descriptor is sector 32 of the partition.
constexpr uint64_t kPartitionOffsets[] = { 0x00000000, 0x0000FB20, 0x00020600, 0x02080000, 0x0FD90000 };
constexpr char kMagic[] = "MICROSOFT*XBOX*MEDIA";
constexpr size_t kMagicSize = sizeof(kMagic) - 1;
constexpr uint8_t kAttributeDirectory = 0x10;
// Garbage limits, far above any real disc (NFSMW: 3 directories, 49
// files). A directory table is addressed by 16-bit dword offsets, so
// it cannot usefully exceed 256 KB; allow some slack.
constexpr uint32_t kMaxDirectoryTable = 1u << 20;
constexpr size_t kMaxEntries = 200000;
constexpr int kMaxDepth = 32;
// All directory tables together. A disc with 50,000 files has a few
// MB of them; without a total, a crafted image whose thousands of
// directories each claim their own 1 MB table makes every open (and
// the disc image scan, which opens each .iso it finds) read gigabytes.
constexpr uint64_t kMaxDirectoryBytes = 64ull << 20;
// Files and folders in an extracted disc, links followed.
constexpr size_t kMaxFolderEntries = 200000;
// Nothing on a disc lies past the end of a dual-layer DVD (XGD3 images
// are 8.7 GB). Data beyond the end of a smaller file means the copy was
// cut short; beyond this, the directory is garbage.
constexpr uint64_t kMaxDiscSize = 0x220000000;
constexpr size_t kHashChunk = 4 << 20;
uint16_t Le16(const uint8_t* p)
{
return uint16_t(p[0] | p[1] << 8);
}
uint32_t Le32(const uint8_t* p)
{
return uint32_t(p[0]) | uint32_t(p[1]) << 8 | uint32_t(p[2]) << 16 | uint32_t(p[3]) << 24;
}
uint32_t Be32(const uint8_t* p)
{
return uint32_t(p[0]) << 24 | uint32_t(p[1]) << 16 | uint32_t(p[2]) << 8 | uint32_t(p[3]);
}
std::string DisplayName(const fs::path& path)
{
std::string name = path.filename().string();
return name.empty() ? path.string() : name;
}
Result Fail(Error error, std::string message, std::string file = {})
{
Result r;
r.error = error;
r.message = std::move(message);
r.file = std::move(file);
return r;
}
// pread until `size` bytes or the end of the file. Returns the bytes
// read (fewer only at the end), or -1 with errno set.
int64_t PreadAll(int fd, void* buffer, size_t size, uint64_t offset)
{
auto p = static_cast<uint8_t*>(buffer);
size_t done = 0;
while (done < size)
{
ssize_t n = pread(fd, p + done, std::min<size_t>(size - done, 1u << 30), off_t(offset + done));
if (n < 0)
{
if (errno == EINTR)
continue;
return -1;
}
if (n == 0)
break;
done += size_t(n);
}
return int64_t(done);
}
bool ReadExact(int fd, uint64_t base, uint64_t limit, uint64_t offset, void* buffer, size_t size, std::string& error)
{
if (offset > limit || size > limit - offset)
{
error = "read past the end of the file";
return false;
}
int64_t got = PreadAll(fd, buffer, size, base + offset);
if (got < 0)
{
error = strerror(errno);
return false;
}
if (uint64_t(got) < size)
{
error = "the data ends early (the file is truncated, or changed while being read)";
return false;
}
return true;
}
void AdviseSequential(int fd)
{
#ifdef POSIX_FADV_SEQUENTIAL
posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL);
#else
(void)fd;
#endif
}
// Disc images.
struct ImageFile
{
int fd = -1;
uint64_t size = 0;
~ImageFile()
{
if (fd >= 0)
close(fd);
}
};
class ImageReader : public FileReader
{
public:
ImageReader(std::shared_ptr<const ImageFile> image, uint64_t offset, uint64_t size)
: image(std::move(image)), offset(offset), size(size)
{
}
bool Read(uint64_t at, void* buffer, size_t count, std::string& error) override
{
return ReadExact(image->fd, offset, size, at, buffer, count, error);
}
private:
std::shared_ptr<const ImageFile> image;
uint64_t offset, size;
};
class ImageSource : public DiscSource
{
public:
ImageSource(std::shared_ptr<const ImageFile> image, fs::path imagePath, std::vector<DiscFile> list, bool cut)
: image(std::move(image))
{
kind = SourceKind::Image;
path = std::move(imagePath);
files = std::move(list);
truncated = cut;
}
std::unique_ptr<FileReader> Open(const DiscFile& file, std::string&) const override
{
return std::make_unique<ImageReader>(image, file.offset, file.size);
}
private:
std::shared_ptr<const ImageFile> image;
};
// A name must be usable as a host path component. Control characters
// and separators only show up when the tree walk has wandered into
// data that isn't a directory, so they mark the image as damaged.
bool ValidName(std::string_view name)
{
if (name.empty() || name == "." || name == "..")
return false;
for (unsigned char c : name)
if (c < 0x20 || c == 0x7F || c == '/' || c == '\\')
return false;
return true;
}
struct TreeWalk
{
const ImageFile& image;
uint64_t partition;
std::vector<DiscFile> files;
size_t entries = 0;
// The first directory whose table lies past the end of the image
// (a truncated copy): skipped, so what precedes the cut still lists.
std::string lostDirectory;
Result Corrupt(const std::string& detail) const
{
return Fail(Error::CorruptImage, "its directory is unreadable (" + detail + ")");
}
static std::string Shown(const std::string& prefix)
{
return prefix.empty() ? "the root directory" : "directory \"" + prefix + "\"";
}
// Reads the directory tree. A directory is a table of entries
// forming a binary tree: each entry holds dword offsets (within the
// table) of its left and right siblings, its data's sector and size,
// attributes and name.
Result Walk(uint32_t rootSector, uint32_t rootSize)
{
struct Directory
{
uint64_t offset;
uint32_t size;
std::string prefix;
int depth;
};
std::vector<Directory> pending{ { partition + rootSector * kSectorSize, rootSize, "", 0 } };
std::unordered_set<uint64_t> tables;
std::vector<uint8_t> table;
std::vector<bool> visited;
std::vector<uint32_t> nodes;
uint64_t tableBytes = 0;
while (!pending.empty())
{
Directory dir = std::move(pending.back());
pending.pop_back();
if (dir.size < 14 || dir.size > kMaxDirectoryTable)
return Corrupt(std::format("{} has size {}", Shown(dir.prefix), dir.size));
if (!tables.insert(dir.offset).second)
return Corrupt(std::format("{} loops back to another directory", Shown(dir.prefix)));
if (dir.offset + dir.size > kMaxDiscSize)
return Corrupt(std::format("{} is at byte {}, past the end of any disc", Shown(dir.prefix), dir.offset));
if (dir.offset + dir.size > image.size)
{
if (lostDirectory.empty())
lostDirectory = Shown(dir.prefix);
continue;
}
if ((tableBytes += dir.size) > kMaxDirectoryBytes)
return Corrupt(std::format("its directory tables add up to more than {}", FormatSize(kMaxDirectoryBytes)));
table.resize(dir.size);
std::string error;
if (!ReadExact(image.fd, 0, image.size, dir.offset, table.data(), table.size(), error))
return Fail(Error::ReadError, "cannot read its directory: " + error);
visited.assign(dir.size / 4 + 1, false);
nodes.assign(1, 0);
while (!nodes.empty())
{
uint32_t at = nodes.back();
nodes.pop_back();
if (at + 14 > dir.size)
return Corrupt(std::format("an entry at {} is outside {}", at, Shown(dir.prefix)));
if (visited[at / 4])
return Corrupt(std::format("the entry at {} in {} is reached twice", at, Shown(dir.prefix)));
visited[at / 4] = true;
const uint8_t* e = &table[at];
uint16_t left = Le16(e + 0), right = Le16(e + 2);
// An empty directory's table is 0xFF filler.
if (left == 0xFFFF && right == 0xFFFF)
{
if (at == 0)
continue;
return Corrupt(std::format("the entry at {} in {} is filler", at, Shown(dir.prefix)));
}
uint32_t sector = Le32(e + 4), size = Le32(e + 8);
uint8_t attributes = e[12], nameLength = e[13];
if (at + 14 + nameLength > dir.size)
return Corrupt(std::format("a name runs past the end of {}", Shown(dir.prefix)));
std::string_view name(reinterpret_cast<const char*>(e + 14), nameLength);
if (!ValidName(name))
return Corrupt(std::format("an invalid name in {}", Shown(dir.prefix)));
if (++entries > kMaxEntries)
return Corrupt("too many entries");
if (left)
nodes.push_back(uint32_t(left) * 4);
if (right)
nodes.push_back(uint32_t(right) * 4);
std::string relative = dir.prefix + std::string(name);
uint64_t data = partition + sector * kSectorSize;
if (attributes & kAttributeDirectory)
{
if (size == 0)
continue; // empty directory
if (dir.depth + 1 > kMaxDepth)
return Corrupt("directories nested too deeply");
pending.push_back({ data, size, relative + "/", dir.depth + 1 });
}
else
{
if (data + size > kMaxDiscSize)
return Corrupt(std::format("\"{}\" is at byte {}, past the end of any disc", relative, data));
files.push_back({ std::move(relative), size, data });
}
}
}
std::sort(files.begin(), files.end(), [](const DiscFile& a, const DiscFile& b) { return a.path < b.path; });
for (size_t i = 1; i < files.size(); i++)
if (files[i].path == files[i - 1].path)
return Corrupt("\"" + files[i].path + "\" is listed twice");
return {};
}
};
bool HasMagicAt(const ImageFile& image, uint64_t offset)
{
char magic[kMagicSize];
std::string error;
return offset + kMagicSize <= image.size
&& ReadExact(image.fd, 0, image.size, offset, magic, kMagicSize, error)
&& memcmp(magic, kMagic, kMagicSize) == 0;
}
// Why a file without an XDVDFS partition isn't usable, in terms of
// what the user probably picked.
Result Unrecognized(const ImageFile& image, const fs::path& path)
{
std::string name = DisplayName(path);
uint8_t head[8] = {};
std::string error;
ReadExact(image.fd, 0, image.size, 0, head, std::min<uint64_t>(sizeof(head), image.size), error);
uint8_t iso9660[5] = {};
bool dvd = image.size >= 0x8006 && ReadExact(image.fd, 0, image.size, 0x8001, iso9660, 5, error)
&& memcmp(iso9660, "CD001", 5) == 0;
if (!memcmp(head, "CON ", 4) || !memcmp(head, "LIVE", 4) || !memcmp(head, "PIRS", 4))
return Fail(Error::UnsupportedFormat, name + " is an Xbox 360 content package (Games on Demand or Xbox Live), "
"not a disc image. Use an image of the game disc (.iso) or its extracted files.");
if (!memcmp(head, "XEX2", 4))
return Fail(Error::NotADiscImage, name + " is an Xbox 360 executable. Choose the folder that holds the "
"extracted disc (default.xex, Movies and NFS), or a disc image.");
if (!memcmp(head, "PK\x03\x04", 4) || !memcmp(head, "7z\xBC\xAF", 4) || !memcmp(head, "Rar!", 4))
return Fail(Error::NotADiscImage, name + " is a compressed archive. Extract it first, then choose the .iso inside.");
// An XGD2 disc starts with a DVD-video partition (ISO 9660); the game
// partition begins at 0xFD90000. A file that has the former but ends
// before the latter was cut short; a large one is a video-only rip.
if (dvd && image.size < kPartitionOffsets[4] + 33 * kSectorSize)
return Fail(Error::TruncatedImage, std::format("{} is incomplete: it ends after {}, before the game data "
"starts. Copy it again, or make a new backup from your disc.", name, FormatSize(image.size)));
if (dvd)
return Fail(Error::UnsupportedFormat, name + " is a DVD image without an Xbox 360 game partition "
"(perhaps only the video partition was copied). Dump the whole disc.");
return Fail(Error::NotADiscImage, name + " is not an Xbox 360 disc image.");
}
Result OpenImage(const fs::path& path, std::unique_ptr<DiscSource>& source, const OpenOptions& options)
{
auto image = std::make_shared<ImageFile>();
image->fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
if (image->fd < 0)
return Fail(Error::NotFound, std::format("Cannot open {}: {}.", path.string(), strerror(errno)), path.string());
struct stat st;
if (fstat(image->fd, &st) != 0)
return Fail(Error::ReadError, std::format("Cannot read {}: {}.", path.string(), strerror(errno)), path.string());
image->size = uint64_t(st.st_size);
AdviseSequential(image->fd);
// Try each partition offset with the magic, first to last, and keep
// the first whose tree reads cleanly. A later offset can hold the
// magic by accident inside an earlier partition's files, so only a
// tree that parses counts.
std::string name = DisplayName(path);
Result firstFailure;
for (uint64_t partition : kPartitionOffsets)
{
uint64_t volume = partition + 32 * kSectorSize;
if (!HasMagicAt(*image, volume))
continue;
uint8_t descriptor[28];
std::string error;
if (!ReadExact(image->fd, 0, image->size, volume, descriptor, sizeof(descriptor), error))
{
if (firstFailure.Ok())
firstFailure = Fail(Error::TruncatedImage, name + " is incomplete: it ends inside its volume descriptor.");
continue;
}
TreeWalk walk{ *image, partition, {}, 0, {} };
Result r = walk.Walk(Le32(descriptor + 20), Le32(descriptor + 24));
if (!r.Ok())
{
if (firstFailure.Ok())
{
firstFailure = r;
firstFailure.message = name + (r.error == Error::CorruptImage ? " is damaged: " : ": ") + r.message + ".";
}
continue;
}
// Every file and directory must lie inside the image. When one
// doesn't, the copy was cut short: say what was lost and how big
// the image should be.
const DiscFile* cut = nullptr;
uint64_t needed = 0;
for (const DiscFile& f : walk.files)
{
if (f.offset + f.size > needed)
needed = f.offset + f.size;
if (f.offset + f.size > image->size && (!cut || f.offset < cut->offset))
cut = &f;
}
bool incomplete = cut || !walk.lostDirectory.empty();
if (walk.files.empty() && !incomplete)
{
if (firstFailure.Ok())
firstFailure = Fail(Error::NotADiscImage, name + " is an empty Xbox disc image.");
continue;
}
Result result;
if (incomplete)
{
result = Fail(Error::TruncatedImage, cut
? std::format("{} is incomplete: it is {} bytes, but its files need at least {} ({} is cut off). ",
name, image->size, needed, cut->path)
: std::format("{} is incomplete: it is {} bytes, and its {} lies beyond that. ",
name, image->size, walk.lostDirectory), cut ? cut->path : std::string());
// FAT32 stops at 4 GB - 1, the classic way a copy loses its end.
result.message += image->size == 0xFFFFFFFFull
? "The drive it was copied to is probably formatted FAT32, which cannot hold files over 4 GB; use exFAT or copy it elsewhere."
: "Copy it again, or make a new backup from your disc.";
if (!options.allowTruncated || walk.files.empty())
return result;
}
source = std::make_unique<ImageSource>(image, path, std::move(walk.files), incomplete);
return result;
}
return firstFailure.Ok() ? Unrecognized(*image, path) : firstFailure;
}
// Extracted folders.
class FolderReader : public FileReader
{
public:
FolderReader(int fd, uint64_t size) : fd(fd), size(size) {}
~FolderReader() override
{
close(fd);
}
bool Read(uint64_t at, void* buffer, size_t count, std::string& error) override
{
return ReadExact(fd, 0, size, at, buffer, count, error);
}
private:
int fd;
uint64_t size;
};
class FolderSource : public DiscSource
{
public:
FolderSource(fs::path root, std::vector<DiscFile> list)
{
kind = SourceKind::Folder;
path = std::move(root);
files = std::move(list);
}
std::unique_ptr<FileReader> Open(const DiscFile& file, std::string& error) const override
{
fs::path host = path / fs::path(file.path);
int fd = open(host.c_str(), O_RDONLY | O_CLOEXEC);
if (fd < 0)
{
error = strerror(errno);
return nullptr;
}
AdviseSequential(fd);
return std::make_unique<FolderReader>(fd, file.size);
}
};
bool Hidden(const fs::path& path)
{
std::string name = path.filename().string();
return !name.empty() && name[0] == '.';
}
bool HoldsXex(const fs::path& dir)
{
std::error_code ec;
for (fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec), end; !ec && it != end; it.increment(ec))
{
std::error_code fileEc;
if (PathEqualsIgnoreCase(it->path().filename().string(), "default.xex") && it->is_regular_file(fileEc))
return true;
}
return false;
}
Result OpenFolder(const fs::path& dir, std::unique_ptr<DiscSource>& source)
{
fs::path root = dir;
if (!HoldsXex(dir))
{
// People often pick the folder around the extracted disc
// ("NFSMW/files/default.xex"); look one level down.
std::vector<fs::path> found;
std::error_code ec;
for (fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec), end; !ec && it != end; it.increment(ec))
{
std::error_code dirEc;
if (!Hidden(it->path()) && it->is_directory(dirEc) && HoldsXex(it->path()))
found.push_back(it->path());
}
if (ec && found.empty())
return Fail(Error::ReadError, std::format("Cannot read the folder {}: {}.", dir.string(), ec.message()), dir.string());
if (found.empty())
return Fail(Error::NotADiscImage, std::format("{} holds no game: there is no default.xex in it or in the "
"folders directly inside it.", dir.string()), dir.string());
if (found.size() > 1)
{
std::sort(found.begin(), found.end());
std::string list;
for (const fs::path& p : found)
list += (list.empty() ? "" : ", ") + DisplayName(p);
return Fail(Error::NotADiscImage, std::format("{} holds several extracted games ({}). Choose one of them.",
dir.string(), list), dir.string());
}
root = found[0];
}
// Linked folders are followed: people keep the big NFS/ folder on
// another drive and link it in. Each folder is identified by its
// device and inode, and a link to one already entered (a parent,
// itself, a sibling) is not entered again, so link loops end.
// Real folders are always entered, whatever order links come
// in: a link met first must not hide the folder it points to.
std::set<std::pair<uint64_t, uint64_t>> entered;
auto firstEntry = [&](const fs::path& dir)
{
struct stat st;
return stat(dir.c_str(), &st) == 0 && entered.insert({ uint64_t(st.st_dev), uint64_t(st.st_ino) }).second;
};
firstEntry(root);
std::vector<DiscFile> files;
size_t entries = 0;
std::error_code ec;
fs::recursive_directory_iterator it(root, fs::directory_options::skip_permission_denied
| fs::directory_options::follow_directory_symlink, ec), end;
for (; !ec && it != end; it.increment(ec))
{
// Hidden entries are host clutter (.DS_Store, AppleDouble "._"
// files on exFAT drives, the install marker), never disc files.
if (Hidden(it->path()))
{
it.disable_recursion_pending();
continue;
}
if (++entries > kMaxFolderEntries)
return Fail(Error::NotADiscImage, root.string() + " holds far more files than a game disc.", root.string());
std::error_code fileEc;
if (it->is_directory(fileEc))
{
bool link = it->is_symlink(fileEc);
if (!firstEntry(it->path()) && link)
it.disable_recursion_pending();
if (it.depth() >= kMaxDepth)
it.disable_recursion_pending();
continue;
}
if (!it->is_regular_file(fileEc))
continue;
uint64_t size = it->file_size(fileEc);
if (fileEc)
return Fail(Error::ReadError, std::format("Cannot read {}: {}.", it->path().string(), fileEc.message()), it->path().string());
files.push_back({ it->path().lexically_relative(root).generic_string(), size, 0 });
}
if (ec)
return Fail(Error::ReadError, std::format("Cannot read the folder {}: {}.", root.string(), ec.message()), root.string());
std::sort(files.begin(), files.end(), [](const DiscFile& a, const DiscFile& b) { return a.path < b.path; });
source = std::make_unique<FolderSource>(root, std::move(files));
return {};
}
}
const DiscFile* DiscSource::Find(std::string_view relativePath) const
{
auto it = std::lower_bound(files.begin(), files.end(), relativePath,
[](const DiscFile& f, std::string_view p) { return f.path < p; });
if (it != files.end() && it->path == relativePath)
return &*it;
for (const DiscFile& f : files)
if (PathEqualsIgnoreCase(f.path, relativePath))
return &f;
return nullptr;
}
Result OpenDiscSource(const std::filesystem::path& path, std::unique_ptr<DiscSource>& source, const OpenOptions& options)
{
source.reset();
std::error_code ec;
fs::file_status status = fs::status(path, ec);
if (status.type() == fs::file_type::not_found)
return Fail(Error::NotFound, path.string() + " does not exist.", path.string());
if (ec)
return Fail(Error::NotFound, std::format("Cannot open {}: {}.", path.string(), ec.message()), path.string());
if (fs::is_directory(status))
return OpenFolder(path, source);
if (!fs::is_regular_file(status))
return Fail(Error::NotADiscImage, path.string() + " is not a file or folder.", path.string());
if (PathEqualsIgnoreCase(path.filename().string(), "default.xex"))
return OpenFolder(path.parent_path().empty() ? fs::path(".") : path.parent_path(), source);
return OpenImage(path, source, options);
}
Result HashFile(const DiscSource& source, const DiscFile& file, Sha256Digest& digest, const std::atomic<bool>* cancel)
{
std::string error;
std::unique_ptr<FileReader> reader = source.Open(file, error);
if (!reader)
return Fail(Error::ReadError, std::format("Cannot open {}: {}.", file.path, error), file.path);
std::vector<uint8_t> buffer(std::min<uint64_t>(kHashChunk, std::max<uint64_t>(file.size, 1)));
Sha256 sha;
for (uint64_t at = 0; at < file.size; at += buffer.size())
{
if (cancel && cancel->load(std::memory_order_relaxed))
return Fail(Error::Cancelled, "Cancelled.");
size_t n = size_t(std::min<uint64_t>(buffer.size(), file.size - at));
if (!reader->Read(at, buffer.data(), n, error))
return Fail(Error::ReadError, std::format("Cannot read {}: {}.", file.path, error), file.path);
sha.Update(buffer.data(), n);
}
digest = sha.Finish();
return {};
}
Result IdentifyXex(const DiscSource& source, XexIdentity& identity)
{
identity = {};
const DiscFile* xex = source.Find("default.xex");
if (!xex)
return Fail(Error::MissingFile, std::format("There is no default.xex at the root of {}.", DisplayName(source.Path())), "default.xex");
identity.file = xex;
std::string error;
std::unique_ptr<FileReader> reader = source.Open(*xex, error);
if (!reader)
return Fail(Error::ReadError, std::format("Cannot open default.xex: {}.", error), xex->path);
// The header (with the execution info) sits in the first chunk.
std::vector<uint8_t> buffer(1 << 20);
Sha256 sha;
for (uint64_t at = 0; at < xex->size; at += buffer.size())
{
size_t n = size_t(std::min<uint64_t>(buffer.size(), xex->size - at));
if (!reader->Read(at, buffer.data(), n, error))
return Fail(Error::ReadError, std::format("Cannot read default.xex: {}.", error), xex->path);
if (at == 0)
identity.titleId = XexTitleId(buffer.data(), n);
sha.Update(buffer.data(), n);
}
identity.sha256 = sha.Finish();
return {};
}
uint32_t XexTitleId(const uint8_t* data, size_t size)
{
// XEX2 header: optional header count at 0x14, then (key, value) pairs
// from 0x18. Key 0x00040006 is the execution info; its title ID is at +12.
if (size < 0x18 || memcmp(data, "XEX2", 4) != 0)
return 0;
uint32_t count = std::min<uint32_t>(Be32(data + 0x14), 256);
for (uint32_t i = 0; i < count; i++)
{
size_t at = 0x18 + size_t(i) * 8;
if (at + 8 > size)
break;
if (Be32(data + at) != 0x00040006)
continue;
uint32_t info = Be32(data + at + 4);
if (uint64_t(info) + 16 > size)
return 0;
return Be32(data + info + 12);
}
return 0;
}
}
+110
View File
@@ -0,0 +1,110 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Where the installer copies the game from: an Xbox 360 disc image (.iso, an
// XDVDFS game partition) or a folder holding an extracted disc. Both list
// their files with disc-relative paths and read them in bounded chunks, so a
// 7.8 GB image is never mapped or loaded whole (a Steam Deck has 16 GB of RAM
// shared with the GPU, and the image may sit on an SD card or USB stick).
//
// Names keep the disc's case (Movies/ is lower case, NFS/ upper case). The
// runtime doesn't care (vfs::Resolve matches case-insensitively), but it tries
// the exact name first, so the installer writes the manifest's spelling.
#pragma once
#include "result.h"
#include "sha256.h"
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace install
{
struct DiscFile
{
std::string path; // relative to the disc root, '/'-separated, disc case
uint64_t size = 0;
uint64_t offset = 0; // image: where the data starts in the image (read in this order); folder: 0
};
enum class SourceKind
{
Image,
Folder,
};
class FileReader
{
public:
virtual ~FileReader() = default;
// Reads exactly `size` bytes at `offset` within the file. False on an
// I/O error or if the data ends early, with `error` saying why.
virtual bool Read(uint64_t offset, void* buffer, size_t size, std::string& error) = 0;
};
class DiscSource
{
public:
virtual ~DiscSource() = default;
SourceKind Kind() const { return kind; }
// The image file, or the folder that holds default.xex (possibly one
// level below the folder that was opened).
const std::filesystem::path& Path() const { return path; }
// Every file, sorted by path; directories are implied by the paths.
const std::vector<DiscFile>& Files() const { return files; }
// Exact match first, then ASCII case-insensitive; nullptr if absent.
const DiscFile* Find(std::string_view relativePath) const;
// Only with OpenOptions::allowTruncated: the image ends before some of
// its files do. CheckSource/Install refuse such a source.
bool Truncated() const { return truncated; }
// Readers are independent of each other (pread on a shared descriptor,
// or a descriptor of their own) and may outlive the source.
virtual std::unique_ptr<FileReader> Open(const DiscFile& file, std::string& error) const = 0;
protected:
SourceKind kind = SourceKind::Folder;
std::filesystem::path path;
std::vector<DiscFile> files;
bool truncated = false;
};
struct OpenOptions
{
// A truncated image normally fails to open. With this set, its listing
// is still returned beside the TruncatedImage error (FindDiscImages
// uses it to show the image as incomplete rather than hide it); files
// past the end fail to read.
bool allowTruncated = false;
};
// Opens `path`: a disc image (XDVDFS game partition at offset 0, 0xFB20,
// 0x20600, 0x2080000 (XGD3) or 0xFD90000 (XGD2)), an extracted disc
// folder (default.xex at its root or one folder down; linked folders
// inside it are followed, each real folder once), or default.xex itself
// (meaning its folder). On failure `source` is null, except as
// OpenOptions::allowTruncated describes.
Result OpenDiscSource(const std::filesystem::path& path, std::unique_ptr<DiscSource>& source, const OpenOptions& options = {});
// Streams a file through SHA-256 in fixed-size chunks.
Result HashFile(const DiscSource& source, const DiscFile& file, Sha256Digest& digest, const std::atomic<bool>* cancel = nullptr);
struct XexIdentity
{
const DiscFile* file = nullptr; // default.xex in the source
Sha256Digest sha256{};
uint32_t titleId = 0; // from the XEX2 execution info; 0 if absent
};
// Finds default.xex at the source's root, hashes it and reads its title
// ID (one pass). MissingFile if there is none.
Result IdentifyXex(const DiscSource& source, XexIdentity& identity);
// The title ID in an XEX2 header's execution info; 0 if it has none.
uint32_t XexTitleId(const uint8_t* data, size_t size);
}
+234
View File
@@ -0,0 +1,234 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See install_cli.h.
#include "install_cli.h"
#include "installer.h"
#include "locate.h"
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <unistd.h>
namespace install
{
namespace
{
namespace fs = std::filesystem;
using Clock = std::chrono::steady_clock;
std::atomic<bool> s_cancel{ false };
void OnInterrupt(int)
{
s_cancel.store(true, std::memory_order_relaxed);
}
// Ctrl-C (or a SIGTERM) asks the install to stop between chunks, so it
// deletes its partial copy instead of leaving gigabytes behind. The
// handler resets itself: a second Ctrl-C kills at once.
class InterruptGuard
{
public:
InterruptGuard()
{
s_cancel.store(false);
struct sigaction sa{};
sa.sa_handler = OnInterrupt;
sa.sa_flags = SA_RESETHAND;
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, &previousInt);
sigaction(SIGTERM, &sa, &previousTerm);
}
~InterruptGuard()
{
sigaction(SIGINT, &previousInt, nullptr);
sigaction(SIGTERM, &previousTerm, nullptr);
}
private:
struct sigaction previousInt{}, previousTerm{};
};
// One rewritten status line on a terminal; a line per file otherwise
// (a log file or the Steam launcher's output).
class ProgressPrinter
{
public:
void operator()(const Progress& p)
{
Clock::time_point now = Clock::now();
bool newFile = p.currentFile != lastFile;
if (newFile)
lastFile = std::string(p.currentFile);
if (p.phase == Progress::Phase::Checking || p.phase == Progress::Phase::Finalizing)
return;
int percent = p.bytesTotal ? int(p.bytesDone * 100 / p.bytesTotal) : 0;
if (!tty)
{
if (newFile && !lastFile.empty())
printf(" %3d%% %s\n", percent, lastFile.c_str());
return;
}
if (!newFile && now - lastPrint < std::chrono::milliseconds(200))
return;
lastPrint = now;
double seconds = std::chrono::duration<double>(now - start).count();
double rate = seconds > 0.5 ? double(p.bytesDone) / seconds : 0.0;
printf("\r %3d%% %s / %s %s/s %s\x1b[K", percent, FormatSize(p.bytesDone).c_str(),
FormatSize(p.bytesTotal).c_str(), FormatSize(uint64_t(rate)).c_str(), lastFile.c_str());
fflush(stdout);
printed = true;
}
void Finish()
{
if (printed)
printf("\n");
fflush(stdout);
}
double Seconds() const
{
return std::chrono::duration<double>(Clock::now() - start).count();
}
private:
bool tty = isatty(STDOUT_FILENO);
bool printed = false;
std::string lastFile;
Clock::time_point start = Clock::now(), lastPrint{};
};
int Failed(const Result& r)
{
fprintf(stderr, "error (%s): %s\n", ErrorName(r.error), r.message.c_str());
return r.error == Error::Cancelled ? 130 : 1;
}
int Usage(const char* message)
{
fprintf(stderr, "%s\n"
"usage: SpeedBreaker --install <image|folder> [--dest <dir>]\n"
" SpeedBreaker --verify <image|folder>\n"
" SpeedBreaker --find-images [folder...]\n"
" SpeedBreaker --where\n", message);
return 2;
}
Result OpenAndCheck(const char* path, std::unique_ptr<DiscSource>& source)
{
Result r = OpenDiscSource(path, source);
if (!r.Ok())
return r;
printf("Source: %s (%s, %zu files)\n", source->Path().c_str(),
source->Kind() == SourceKind::Image ? "disc image" : "folder", source->Files().size());
return {};
}
int RunInstall(const char* sourcePath, const fs::path& destination)
{
std::unique_ptr<DiscSource> source;
if (Result r = OpenAndCheck(sourcePath, source); !r.Ok())
return Failed(r);
printf("Installing %s to %s\n", FormatSize(GameManifest().totalBytes).c_str(), destination.c_str());
InterruptGuard guard;
ProgressPrinter printer;
Result r = Install(*source, destination, std::ref(printer), &s_cancel);
printer.Finish();
if (!r.Ok())
return Failed(r);
printf("Installed and verified in %.0f s: %s\n", printer.Seconds(), destination.c_str());
// So the game finds it there (the default location clears the record).
if (!RecordInstallPath(destination))
fprintf(stderr, "Couldn't record the install location; start the game with NFSMW_GAME_DIR=%s\n", destination.c_str());
return 0;
}
int RunVerify(const char* sourcePath)
{
std::unique_ptr<DiscSource> source;
if (Result r = OpenAndCheck(sourcePath, source); !r.Ok())
return Failed(r);
InterruptGuard guard;
ProgressPrinter printer;
Result r = Verify(*source, std::ref(printer), &s_cancel);
printer.Finish();
if (!r.Ok())
return Failed(r);
printf("All %zu files match the original disc (%.0f s).\n", GameManifest().files.size(), printer.Seconds());
return 0;
}
int RunFindImages(const std::vector<fs::path>& folders)
{
std::vector<fs::path> where = folders.empty() ? DiscImageSearchFolders() : folders;
printf("Looking in %zu folders (and one level below them)...\n", where.size());
InterruptGuard guard;
std::vector<DiscImageCandidate> images = FindDiscImages(where, 1ull << 30, &s_cancel);
for (const DiscImageCandidate& image : images)
{
printf(" %-6s %s (%s)\n", image.status.Ok() ? "ready" : "no", image.path.c_str(),
FormatSize(image.size).c_str());
if (!image.status.Ok())
printf(" %s: %s\n", ErrorName(image.status.error), image.status.message.c_str());
}
if (images.empty())
printf("No Xbox 360 disc images found.\n");
return images.empty() ? 1 : 0;
}
int RunWhere()
{
std::optional<GameInstall> game = FindGameInstall();
if (!game)
{
printf("No game install found. Install one with --install <image|folder>.\n");
return 1;
}
const char* origin = game->origin == InstallOrigin::Environment ? "NFSMW_GAME_DIR"
: game->origin == InstallOrigin::Development ? "development checkout" : "installed";
printf("%s (%s%s)\n", game->path.c_str(), origin, game->marker ? ", verified by the installer" : "");
return 0;
}
}
std::optional<int> RunInstallCommand(int argc, char** argv)
{
for (int i = 1; i < argc; i++)
{
std::string_view arg = argv[i];
if (arg == "--install")
{
if (i + 1 >= argc)
return Usage("--install needs the disc image or folder to install from.");
const char* source = argv[i + 1];
fs::path destination = DefaultInstallPath();
for (int j = i + 2; j < argc; j++)
{
if (std::string_view(argv[j]) == "--dest" && j + 1 < argc)
destination = argv[++j];
else
return Usage((std::string("unexpected argument: ") + argv[j]).c_str());
}
return RunInstall(source, destination);
}
if (arg == "--verify")
{
if (i + 1 >= argc)
return Usage("--verify needs the disc image or folder to check.");
return RunVerify(argv[i + 1]);
}
if (arg == "--find-images")
return RunFindImages(std::vector<fs::path>(argv + i + 1, argv + argc));
if (arg == "--where")
return RunWhere();
}
return std::nullopt;
}
}
+19
View File
@@ -0,0 +1,19 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The installer on the command line, for scripts, SSH sessions and machines
// without the first-run UI:
// SpeedBreaker --install <image|folder> [--dest <dir>] copy and verify (default dest: GetUserPath()/game)
// SpeedBreaker --verify <image|folder> hash-check every file without copying
// SpeedBreaker --find-images [folder...] list disc images in the usual places (or these)
// SpeedBreaker --where print where the game install was found
// Ctrl-C cancels an install cleanly (the partial copy is removed).
#pragma once
#include <optional>
namespace install
{
// If argv holds one of the commands above, runs it and returns the exit
// code for main to return (0 success, 1 failure, 2 usage, 130 cancelled);
// otherwise returns nullopt and the game starts as usual.
std::optional<int> RunInstallCommand(int argc, char** argv);
}
+882
View File
@@ -0,0 +1,882 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See installer.h.
#include "installer.h"
#include <user/paths.h>
#ifdef __APPLE__
#include <TargetConditionals.h>
#endif
#if defined(__APPLE__) && TARGET_OS_IOS
#include <platform/ios_files.h>
#endif
#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <format>
#include <fstream>
#include <set>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <unistd.h>
namespace install
{
namespace
{
namespace fs = std::filesystem;
constexpr const char* kGameName = "Need for Speed: Most Wanted";
// The release manifest.inc was generated from (config/nfsmw.toml).
constexpr const char* kRelease = "the USA Xbox 360 release";
constexpr size_t kChunk = 4 << 20;
// Room beyond the files themselves: directory blocks, the marker, and
// not filling the user's disk to the last byte.
constexpr uint64_t kSpaceMargin = 64ull << 20;
// Marks "<destination>.partial" as the installer's own, so a leftover
// from a crash is deleted on the next attempt and nothing else is.
constexpr const char* kPartialTag = ".nfsmw-install-partial";
Result Fail(Error error, std::string message, std::string file = {})
{
Result r;
r.error = error;
r.message = std::move(message);
r.file = std::move(file);
return r;
}
void Report(const ProgressCallback& progress, const Progress& p)
{
if (progress)
progress(p);
}
bool Cancelled(const std::atomic<bool>* cancel)
{
return cancel && cancel->load(std::memory_order_relaxed);
}
const char* What(const DiscSource& source)
{
return source.Kind() == SourceKind::Image ? "This disc image" : "This folder";
}
bool WriteAll(int fd, const void* data, size_t size)
{
auto p = static_cast<const uint8_t*>(data);
while (size)
{
ssize_t n = write(fd, p, std::min<size_t>(size, 1u << 30));
if (n < 0)
{
if (errno == EINTR)
continue;
return false;
}
p += n;
size -= size_t(n);
}
return true;
}
// A failed write or create, by what the user can do about it.
Result WriteFailure(int error, const fs::path& path)
{
if (error == ENOSPC || error == EDQUOT)
return Fail(Error::NotEnoughSpace, std::format("The disk filled up while installing ({}). Free some space "
"and try again.", path.string()), path.string());
if (error == EACCES || error == EPERM || error == EROFS)
return Fail(Error::DestinationNotWritable, std::format("Cannot write {}: {}. Choose another folder.",
path.string(), strerror(error)), path.string());
return Fail(Error::WriteError, std::format("Cannot write {}: {}.", path.string(), strerror(error)), path.string());
}
void SyncDirectory(const fs::path& dir)
{
int fd = open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0)
{
fsync(fd);
close(fd);
}
}
// Anything at `path`, a dangling link included.
bool Present(const fs::path& path)
{
std::error_code ec;
return fs::exists(fs::symlink_status(path, ec));
}
bool IsEmptyDirectory(const fs::path& dir)
{
std::error_code ec;
return fs::is_directory(dir, ec) && fs::is_empty(dir, ec) && !ec;
}
// Only hidden entries (or none): what Finder (.DS_Store, .localized)
// or a copy to an exFAT drive (._ files) leaves in a folder the
// player thinks is empty.
bool HoldsOnlyHidden(const fs::path& dir)
{
std::error_code ec;
for (fs::directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec))
{
std::string name = it->path().filename().string();
if (name.empty() || name[0] != '.')
return false;
}
return !ec;
}
// Files in the order they're read: disc order for an image (sequential
// reads, which matters on a USB stick or a hard drive), then by path.
std::vector<std::pair<const ManifestFile*, const DiscFile*>> ReadOrder(const DiscSource& source, const Manifest& manifest)
{
std::vector<std::pair<const ManifestFile*, const DiscFile*>> order;
for (const ManifestFile& mf : manifest.files)
order.push_back({ &mf, source.Find(mf.path) });
std::sort(order.begin(), order.end(), [](const auto& a, const auto& b)
{
if (a.second->offset != b.second->offset)
return a.second->offset < b.second->offset;
return a.first->path < b.first->path;
});
return order;
}
// Streams one file through SHA-256 in fixed chunks, writing each
// chunk to `fd` too when it is valid: the copy and the check are one
// read of the source.
Result StreamFile(const DiscSource& source, const DiscFile& file, const ManifestFile& expected, int fd,
const fs::path& target, std::vector<uint8_t>& buffer, Progress& p, const ProgressCallback& progress,
const std::atomic<bool>* cancel)
{
std::string error;
std::unique_ptr<FileReader> reader = source.Open(file, error);
if (!reader)
return Fail(Error::ReadError, std::format("Cannot open {} in the source: {}.", file.path, error), file.path);
Sha256 sha;
for (uint64_t at = 0; at < file.size;)
{
if (Cancelled(cancel))
return Fail(Error::Cancelled, "Installation cancelled.");
size_t n = size_t(std::min<uint64_t>(buffer.size(), file.size - at));
if (!reader->Read(at, buffer.data(), n, error))
return Fail(Error::ReadError, std::format("Cannot read {} from the source: {}. If the disc image is on "
"a removable drive, check that it is still connected.", file.path, error), file.path);
sha.Update(buffer.data(), n);
if (fd >= 0 && !WriteAll(fd, buffer.data(), n))
return WriteFailure(errno, target);
at += n;
p.bytesDone += n;
Report(progress, p);
}
std::string found = ToHex(sha.Finish());
if (found != expected.sha256)
{
Result r = Fail(Error::CorruptFile, std::format("{} does not match the original disc: its SHA-256 is {}, "
"expected {}. The dump is damaged; dump the disc again.", std::string(expected.path), found,
std::string(expected.sha256)), std::string(expected.path));
r.foundHash = found;
return r;
}
return {};
}
// Install's own directories: "<dest>.partial" while copying,
// "<dest>.previous" for the old install while the new one moves in.
struct Layout
{
fs::path destination, parent, partial, previous;
};
// Where `requested` really is: absolute, with links resolved (a game
// folder linked to an SD card installs beside the link's target, on
// the same file system, where the final rename is atomic), and not
// something an install can't take the place of. Changes nothing.
Result ResolveDestination(const fs::path& requested, Layout& layout)
{
std::error_code ec;
if (requested.empty())
return Fail(Error::DestinationNotWritable, "Choose a folder to install into.");
fs::path dest = fs::absolute(requested, ec);
if (ec)
return Fail(Error::DestinationNotWritable, std::format("Cannot use {}: {}.", requested.string(),
ec.message()), requested.string());
if (dest.filename().empty())
dest = dest.parent_path();
// A link to a folder that isn't there (a drive that isn't
// connected): the final rename would replace the link, and
// following it would create folders where the drive mounts.
if (fs::is_symlink(dest, ec) && !fs::exists(dest, ec))
{
std::error_code linkEc;
fs::path target = fs::read_symlink(dest, linkEc);
return Fail(Error::DestinationNotWritable, std::format("{} is a link to {}, which does not exist. Connect "
"the drive it points to, or choose another folder.", dest.string(), target.string()), dest.string());
}
fs::path resolved = fs::weakly_canonical(dest, ec);
dest = ec ? dest.lexically_normal() : resolved;
if (dest.filename().empty())
dest = dest.parent_path();
if (dest.empty() || dest == dest.root_path())
return Fail(Error::DestinationNotWritable, "Choose a folder to install into, not the root of a drive.");
layout.destination = dest;
layout.parent = dest.parent_path();
layout.partial = layout.parent / (dest.filename().string() + ".partial");
layout.previous = layout.parent / (dest.filename().string() + ".previous");
// The top of a mounted drive can't be renamed, and its siblings
// are on the drive above it: the copy would fill the wrong disk
// (on SteamOS, /run/media is in RAM) and then fail to move over.
struct stat destStat, parentStat;
if (stat(dest.c_str(), &destStat) == 0 && S_ISDIR(destStat.st_mode)
&& stat(layout.parent.c_str(), &parentStat) == 0 && destStat.st_dev != parentStat.st_dev)
return Fail(Error::DestinationNotWritable, std::format("{} is a whole drive. Choose a folder on it instead, "
"such as {}.", dest.string(), (dest / kUserFolderName).string()), dest.string());
return {};
}
// The nearest folder at or above `path` that exists: where missing
// folders would be created. Empty if a file is in the way (`file`).
fs::path NearestFolder(fs::path path, fs::path& file)
{
std::error_code ec;
while (!path.empty())
{
fs::file_status status = fs::status(path, ec);
if (fs::is_directory(status))
return path;
if (fs::exists(status))
{
file = path;
return {};
}
if (path == path.parent_path())
break;
path = path.parent_path();
}
return {};
}
// A leftover "<dest>.previous" that Recover may put back or retire:
// a previous install (it has the marker), or the hidden files a
// replaced "empty" folder held.
bool PreviousIsOurs(const fs::path& previous)
{
std::error_code ec;
return !fs::is_symlink(previous, ec) && fs::is_directory(previous, ec)
&& (ReadInstallMarker(previous) || HoldsOnlyHidden(previous));
}
bool PartialIsOurs(const fs::path& partial)
{
std::error_code ec;
return !fs::is_symlink(partial, ec) && fs::is_directory(partial, ec)
&& (Present(partial / kPartialTag) || ReadInstallMarker(partial) || IsEmptyDirectory(partial));
}
// The destination as Install would find it, without changing
// anything. Before Recover has run (`recovered` false), a previous
// install left at .previous counts as what it will become.
Result Inspect(const Layout& layout, const Manifest& manifest, bool recovered, DestinationInfo& info)
{
std::error_code ec;
const fs::path& dest = layout.destination;
info.path = dest;
info.required = manifest.totalBytes + kSpaceMargin;
info.replacing = false;
bool pendingPrevious = !recovered && PreviousIsOurs(layout.previous);
bool destinationEmpty = true;
fs::file_status status = fs::status(dest, ec);
if (fs::exists(status))
{
if (!fs::is_directory(status))
return Fail(Error::DestinationNotEmpty, std::format("{} is a file. Choose a folder to install into.",
dest.string()), dest.string());
// Only a previous install or an empty folder may be replaced.
// Hidden files like .DS_Store don't count; they are kept.
bool install = ReadInstallMarker(dest).has_value();
if (!install && !HoldsOnlyHidden(dest))
return Fail(Error::DestinationNotEmpty, std::format("{} already holds other files. Choose an empty "
"folder or a new one.", dest.string()), dest.string());
info.replacing = install;
destinationEmpty = IsEmptyDirectory(dest);
}
info.replacing = info.replacing || pendingPrevious;
// A non-empty destination is moved to .previous while the new
// install takes its place, so that name must be free (a previous
// install still waiting there is put back or retired first).
if (!destinationEmpty && !pendingPrevious && Present(layout.previous) && !IsEmptyDirectory(layout.previous))
return Fail(Error::DestinationNotEmpty, std::format("{} is in the way (the installer moves the current "
"install there while the new one takes its place). Move or delete it.", layout.previous.string()),
layout.previous.string());
if (Present(layout.partial) && !PartialIsOurs(layout.partial))
return Fail(Error::DestinationNotEmpty, std::format("{} is in the way (the installer copies into it "
"first). Move or delete it.", layout.partial.string()), layout.partial.string());
fs::path file;
fs::path folder = NearestFolder(layout.parent, file);
if (folder.empty())
return Fail(Error::DestinationNotWritable, file.empty()
? std::format("Cannot install to {}.", dest.string())
: std::format("Cannot install to {}: {} is a file, not a folder.", dest.string(), file.string()),
dest.string());
if (access(folder.c_str(), W_OK | X_OK) != 0)
return Fail(Error::DestinationNotWritable, std::format("Cannot write to {}: {}. Choose another folder.",
folder.string(), strerror(errno)), folder.string());
return {};
}
uint64_t TreeSize(const fs::path& dir)
{
uint64_t total = 0;
std::error_code ec;
for (fs::recursive_directory_iterator it(dir, ec), end; !ec && it != end; it.increment(ec))
{
std::error_code fileEc;
if (it->is_regular_file(fileEc) && !it->is_symlink(fileEc))
total += it->file_size(fileEc);
}
return total;
}
// Free space in `folder`; nullopt if it can't be told.
std::optional<uint64_t> FreeSpace(const fs::path& folder)
{
#if defined(__APPLE__) && TARGET_OS_IOS
// iOS keeps space in purgeable caches that statvfs counts as used,
// and frees it for something the player asked for: the figure
// Apple says to check before a large download.
if (std::optional<uint64_t> important = platform::ios::AvailableCapacityForImportantUsage(folder))
return important;
#endif
struct statvfs vfs;
if (statvfs(folder.c_str(), &vfs) != 0)
return std::nullopt;
return uint64_t(vfs.f_bavail) * vfs.f_frsize;
}
// `reclaimable`: bytes of the installer's own leftover .partial,
// which Install deletes before copying.
Result CheckSpace(const Layout& layout, DestinationInfo& info, uint64_t reclaimable)
{
info.available = UINT64_MAX;
fs::path file;
fs::path folder = NearestFolder(layout.parent, file);
std::optional<uint64_t> space = folder.empty() ? std::nullopt : FreeSpace(folder);
if (!space)
return {}; // can't tell; a full disk still fails cleanly mid-copy
info.available = *space + reclaimable;
if (info.available >= info.required)
return {};
return Fail(Error::NotEnoughSpace, std::format("Not enough disk space: the game needs {}, and {} has {} free. "
"Free up {} or choose another drive.", FormatSize(info.required), folder.string(),
FormatSize(info.available), FormatSize(info.required - info.available)), folder.string());
}
// One install at a time per parent folder: an exclusive flock on the
// folder, held until Install returns. Two installers (the first-run
// screen and --install) would otherwise delete each other's partial
// copy, or one's cleanup would delete the other's finished install.
// Where the file system can't lock, installs go unlocked.
class FolderLock
{
public:
explicit FolderLock(const fs::path& dir)
{
fd = open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (fd >= 0 && flock(fd, LOCK_EX | LOCK_NB) != 0)
{
busy = errno == EWOULDBLOCK;
close(fd);
fd = -1;
}
}
~FolderLock()
{
if (fd >= 0)
close(fd);
}
FolderLock(const FolderLock&) = delete;
FolderLock& operator=(const FolderLock&) = delete;
bool Busy() const { return busy; }
private:
int fd = -1;
bool busy = false;
};
// The manifest's spelling of a folder it has files in, matched
// case-insensitively; nullopt if `relative` isn't one.
std::optional<std::string> ManifestFolder(const Manifest& manifest, std::string_view relative)
{
for (const ManifestFile& f : manifest.files)
if (f.path.size() > relative.size() && f.path[relative.size()] == '/'
&& PathEqualsIgnoreCase(f.path.substr(0, relative.size()), relative))
return std::string(f.path.substr(0, relative.size()));
return std::nullopt;
}
// Moves everything in the old install `from` that isn't the game's
// into the new install `to`: DLC (xam.cpp keeps it in the game
// folder's dlc/), a disc image or notes the player put there, files
// beside the game's in NFS/. Game files and the marker stay behind to
// be deleted with the old install, and so does a link standing in for
// one of the game's folders (only the link goes; what it points to is
// left alone). False if anything could not be moved: the old install
// must then be kept.
bool CarryOver(const fs::path& from, const fs::path& to, const std::string& prefix, const Manifest& manifest)
{
// Listed first: entries leave the folder while it is gone through.
std::vector<fs::directory_entry> entries;
std::error_code ec;
for (fs::directory_iterator it(from, ec), end; !ec && it != end; it.increment(ec))
entries.push_back(*it);
if (ec)
{
fprintf(stderr, "[install] cannot list %s: %s\n", from.c_str(), ec.message().c_str());
return false;
}
bool complete = true;
for (const fs::directory_entry& entry : entries)
{
std::string name = entry.path().filename().string();
std::string relative = prefix + name;
if (prefix.empty() && (name == kInstallMarker || name == kPartialTag))
continue;
if (manifest.Find(relative))
continue;
std::error_code typeEc;
if (std::optional<std::string> folder = ManifestFolder(manifest, relative))
{
if (!entry.is_symlink(typeEc) && entry.is_directory(typeEc))
complete &= CarryOver(entry.path(), to / fs::path(*folder).filename(), relative + "/", manifest);
continue;
}
fs::path target = to / name;
if (Present(target))
{
fprintf(stderr, "[install] cannot move %s: the new install already has %s\n", entry.path().c_str(),
target.c_str());
complete = false;
continue;
}
fs::rename(entry.path(), target, typeEc);
if (typeEc)
{
fprintf(stderr, "[install] cannot move %s into the new install: %s\n", entry.path().c_str(),
typeEc.message().c_str());
complete = false;
}
}
return complete;
}
// Deletes the old install at .previous once the new one is in place,
// after carrying the player's files over. If any could not be moved,
// it stays (logged), and the next install or startup tries again.
void RetirePrevious(const Layout& layout, const Manifest& manifest)
{
if (!CarryOver(layout.previous, layout.destination, "", manifest))
{
fprintf(stderr, "[install] kept the previous install at %s: it still holds files that could not be moved "
"into %s\n", layout.previous.c_str(), layout.destination.c_str());
return;
}
std::error_code ec;
fs::remove_all(layout.previous, ec);
if (ec)
fprintf(stderr, "[install] could not delete the previous install at %s: %s\n", layout.previous.c_str(),
ec.message().c_str());
}
// Undoes or finishes a reinstall interrupted between MoveIntoPlace's
// two renames, or before the old install was retired.
void Recover(const Layout& layout, const Manifest& manifest)
{
if (!PreviousIsOurs(layout.previous))
return;
std::error_code ec;
if (!fs::exists(layout.destination, ec) || IsEmptyDirectory(layout.destination))
{
// Nothing took its place: put it back (a rename replaces an
// empty folder).
fs::rename(layout.previous, layout.destination, ec);
fprintf(stderr, "[install] restored %s from an interrupted reinstall%s%s\n", layout.destination.c_str(),
ec ? ": " : "", ec ? ec.message().c_str() : "");
}
else if (ReadInstallMarker(layout.destination))
{
// The new install made it into place: finish the job.
RetirePrevious(layout, manifest);
}
// Otherwise the destination holds something else: leave both
// alone (Inspect refuses such a destination).
}
// 0, or the errno of the failure.
int WriteMarker(const fs::path& dir, const Manifest& manifest)
{
std::string text = std::format(
"# SpeedBreaker: a verified install of the game. Written last by the installer,\n"
"# after every file matched the manifest; FindGameInstall() requires it.\n"
"manifest_version = \"{}\"\n"
"xex_sha256 = \"{}\"\n"
"files = {}\n"
"bytes = {}\n",
manifest.version, manifest.Xex().sha256, manifest.files.size(), manifest.totalBytes);
fs::path path = dir / kInstallMarker;
int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (fd < 0)
return errno;
int error = 0;
if (!WriteAll(fd, text.data(), text.size()) || fsync(fd) != 0)
error = errno ? errno : EIO;
if (close(fd) != 0 && !error)
error = errno ? errno : EIO;
return error;
}
// Moves the finished copy into place. rename() replaces a missing or
// empty destination atomically; anything else there (a previous
// install, or the hidden files of an "empty" folder) is first moved
// aside (and restored if the second rename fails), then retired.
Result MoveIntoPlace(const Layout& layout, const Manifest& manifest)
{
std::error_code ec;
bool replacing = fs::exists(layout.destination, ec) && !IsEmptyDirectory(layout.destination);
if (replacing)
{
fs::rename(layout.destination, layout.previous, ec);
if (ec)
return Fail(Error::WriteError, std::format("Cannot move the previous install at {} aside: {}.",
layout.destination.string(), ec.message()), layout.destination.string());
}
fs::rename(layout.partial, layout.destination, ec);
if (ec)
{
std::error_code restoreEc;
if (replacing)
fs::rename(layout.previous, layout.destination, restoreEc);
return Fail(Error::WriteError, std::format("Cannot move the finished install to {}: {}.",
layout.destination.string(), ec.message()), layout.destination.string());
}
SyncDirectory(layout.parent);
if (replacing)
RetirePrevious(layout, manifest);
return {};
}
}
Result CheckGameVersion(const DiscSource& source, const Manifest& manifest, XexIdentity* identity)
{
XexIdentity local;
XexIdentity& id = identity ? *identity : local;
Result r = IdentifyXex(source, id);
if (r.error == Error::MissingFile)
{
// Original Xbox discs boot default.xbe.
if (source.Find("default.xbe"))
return Fail(Error::WrongGame, std::format("{} holds an original Xbox game, not {} for Xbox 360.",
What(source), kGameName), "default.xbe");
return Fail(Error::NotADiscImage, std::format("{} has no default.xex: it is not an Xbox 360 game.",
What(source)), "default.xex");
}
if (!r.Ok())
return r;
const ManifestFile& xex = manifest.Xex();
std::string found = ToHex(id.sha256);
if (found == xex.sha256)
return {};
if (id.titleId != manifest.titleId)
{
r = Fail(Error::WrongGame, id.titleId
? std::format("{} is not {}: it holds another game (title ID {:08X}; {} is {:08X}).", What(source),
kGameName, id.titleId, kGameName, manifest.titleId)
: std::format("{} is not {}: its default.xex is not an Xbox 360 game executable.", What(source), kGameName),
id.file->path);
r.foundHash = found;
return r;
}
// Same title, different executable: another region or revision. The
// recompiled code matches one executable byte for byte, so nothing
// else can run, whatever else is on the disc.
r = Fail(Error::UnsupportedVersion, std::format("{} is {}, but not the version this build supports: its "
"default.xex has SHA-256 {}. Only {} is supported (default.xex {}){}.", What(source), kGameName, found,
kRelease, std::string(xex.sha256),
id.file->size == xex.size ? ", unless this one is a damaged dump" : ""), id.file->path);
r.foundHash = found;
return r;
}
Result CheckSource(const DiscSource& source, const Manifest& manifest)
{
if (source.Truncated())
return Fail(Error::TruncatedImage, "This disc image is incomplete: it ends before some of its files do. "
"Copy it again, or make a new backup from your disc.", source.Path().string());
Result r = CheckGameVersion(source, manifest);
if (!r.Ok())
return r;
const ManifestFile* missing = nullptr;
const ManifestFile* wrongSize = nullptr;
const DiscFile* wrongFile = nullptr;
size_t missingCount = 0, wrongCount = 0;
for (const ManifestFile& mf : manifest.files)
{
const DiscFile* f = source.Find(mf.path);
if (!f)
{
missing = missing ? missing : &mf;
missingCount++;
}
else if (f->size != mf.size)
{
if (!wrongSize)
{
wrongSize = &mf;
wrongFile = f;
}
wrongCount++;
}
}
auto more = [](size_t n) { return n > 1 ? std::format(" (and {} more)", n - 1) : std::string(); };
if (missing)
return Fail(Error::MissingFile, std::format("{} is incomplete: {} is missing{}. Dump the disc again, or copy "
"every file of the extracted disc.", What(source), std::string(missing->path), more(missingCount)),
std::string(missing->path));
if (wrongSize)
return Fail(Error::CorruptFile, std::format("{} is damaged: {} is {} bytes, but should be {}{}. Dump the "
"disc again.", What(source), std::string(wrongSize->path), wrongFile->size, wrongSize->size,
more(wrongCount)), std::string(wrongSize->path));
return {};
}
Result CheckDestination(const std::filesystem::path& destination, DestinationInfo* info, const Manifest& manifest)
{
DestinationInfo local;
DestinationInfo& out = info ? *info : local;
out = {};
out.required = manifest.totalBytes + kSpaceMargin;
Layout layout;
Result r = ResolveDestination(destination, layout);
if (r.Ok())
r = Inspect(layout, manifest, false, out);
if (r.Ok())
r = CheckSpace(layout, out, PartialIsOurs(layout.partial) ? TreeSize(layout.partial) : 0);
return r;
}
void RecoverInterruptedInstall(const std::filesystem::path& destination, const Manifest& manifest)
{
Layout layout;
if (!ResolveDestination(destination, layout).Ok() || !Present(layout.previous))
return;
FolderLock lock(layout.parent);
if (!lock.Busy()) // otherwise an install is running and owns these folders
Recover(layout, manifest);
}
Result Install(const DiscSource& source, const std::filesystem::path& destination,
const ProgressCallback& progress, const std::atomic<bool>* cancel, const Manifest& manifest)
{
Progress p;
p.bytesTotal = manifest.totalBytes;
p.filesTotal = manifest.files.size();
Report(progress, p);
Result r = CheckSource(source, manifest);
if (!r.Ok())
return r;
if (Cancelled(cancel))
return Fail(Error::Cancelled, "Installation cancelled.");
// Refuse what can be refused before anything on disk changes.
Layout layout;
DestinationInfo info;
r = ResolveDestination(destination, layout);
if (r.Ok())
r = Inspect(layout, manifest, false, info);
if (r.Ok())
r = CheckSpace(layout, info, PartialIsOurs(layout.partial) ? TreeSize(layout.partial) : 0);
if (!r.Ok())
return r;
std::error_code ec;
fs::create_directories(layout.parent, ec);
if (ec)
return Fail(Error::DestinationNotWritable, std::format("Cannot create {}: {}.", layout.parent.string(),
ec.message()), layout.parent.string());
FolderLock lock(layout.parent);
if (lock.Busy())
return Fail(Error::DestinationNotWritable, std::format("Another install into {} is running. Wait for it to "
"finish, then try again.", layout.parent.string()), layout.parent.string());
// Under the lock: finish an interrupted reinstall, look again, and
// clear this installer's own leftover copy.
Recover(layout, manifest);
r = Inspect(layout, manifest, true, info);
if (!r.Ok())
return r;
if (Present(layout.partial))
{
fs::remove_all(layout.partial, ec);
if (ec)
return Fail(Error::DestinationNotWritable, std::format("Cannot remove the unfinished install at {}: {}.",
layout.partial.string(), ec.message()), layout.partial.string());
}
r = CheckSpace(layout, info, 0);
if (!r.Ok())
return r;
if (!fs::create_directory(layout.partial, ec) || ec)
return Fail(Error::DestinationNotWritable, std::format("Cannot create {}: {}. Choose another folder.",
layout.partial.string(), ec ? ec.message() : "it already exists"), layout.partial.string());
auto failed = [&](Result result)
{
std::error_code removeEc;
fs::remove_all(layout.partial, removeEc);
return result;
};
int tag = open((layout.partial / kPartialTag).c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
if (tag < 0)
return failed(WriteFailure(errno, layout.partial / kPartialTag));
close(tag);
#if defined(__APPLE__) && TARGET_OS_IOS
// 7 GB the player's disc image can make again: not for iCloud or a
// computer's backup. Marked before the copy, so a backup taken while
// it runs skips it too.
platform::ios::ExcludeFromBackup(layout.partial);
#endif
std::vector<uint8_t> buffer(kChunk);
std::set<fs::path> directories{ layout.partial };
p.phase = Progress::Phase::Copying;
for (auto [mf, file] : ReadOrder(source, manifest))
{
// The manifest's spelling, not the source's: see disc_source.h.
fs::path target = layout.partial / fs::path(std::string(mf->path));
if (directories.insert(target.parent_path()).second)
{
fs::create_directories(target.parent_path(), ec);
if (ec)
return failed(WriteFailure(ec.value(), target.parent_path()));
}
p.currentFile = mf->path;
Report(progress, p);
int fd = open(target.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
if (fd < 0)
return failed(WriteFailure(errno, target));
r = StreamFile(source, *file, *mf, fd, target, buffer, p, progress, cancel);
// fsync before the rename below makes the install durable: after a
// power cut the marker never stands beside files that weren't
// written out.
if (r.Ok() && fsync(fd) != 0)
r = WriteFailure(errno, target);
if (close(fd) != 0 && r.Ok())
r = WriteFailure(errno, target);
if (!r.Ok())
return failed(r);
p.filesDone++;
}
p.phase = Progress::Phase::Finalizing;
p.currentFile = {};
Report(progress, p);
if (Cancelled(cancel))
return failed(Fail(Error::Cancelled, "Installation cancelled."));
if (int error = WriteMarker(layout.partial, manifest))
return failed(WriteFailure(error, layout.partial / kInstallMarker));
fs::remove(layout.partial / kPartialTag, ec);
for (const fs::path& dir : directories)
SyncDirectory(dir);
r = MoveIntoPlace(layout, manifest);
if (!r.Ok())
return failed(r);
#if defined(__APPLE__) && TARGET_OS_IOS
// The game folder too: the rename keeps the mark, but a lost one
// would put 7 GB into the player's iCloud backup.
platform::ios::ExcludeFromBackup(layout.destination);
#endif
fprintf(stderr, "[install] installed %zu files (%s) to %s\n", manifest.files.size(),
FormatSize(manifest.totalBytes).c_str(), layout.destination.c_str());
return {};
}
Result Verify(const DiscSource& source, const ProgressCallback& progress, const std::atomic<bool>* cancel,
const Manifest& manifest)
{
Progress p;
p.bytesTotal = manifest.totalBytes;
p.filesTotal = manifest.files.size();
Report(progress, p);
Result r = CheckSource(source, manifest);
if (!r.Ok())
return r;
std::vector<uint8_t> buffer(kChunk);
p.phase = Progress::Phase::Verifying;
for (auto [mf, file] : ReadOrder(source, manifest))
{
p.currentFile = mf->path;
Report(progress, p);
r = StreamFile(source, *file, *mf, -1, {}, buffer, p, progress, cancel);
if (!r.Ok())
return r;
p.filesDone++;
}
p.phase = Progress::Phase::Finalizing;
p.currentFile = {};
Report(progress, p);
return {};
}
std::filesystem::path DefaultInstallPath()
{
return GetUserPath() / "game";
}
std::optional<std::string> ReadInstallMarker(const std::filesystem::path& dir)
{
// A few lines of text: read no more than that from whatever file
// happens to have the name.
std::ifstream f(dir / kInstallMarker, std::ios::binary);
char text[4096];
f.read(text, sizeof(text));
std::string_view content(text, size_t(std::max<std::streamsize>(f.gcount(), 0)));
constexpr std::string_view key = "manifest_version";
while (!content.empty())
{
// manifest_version = "<version>"
size_t end = content.find('\n');
std::string_view line = content.substr(0, end);
content = end == std::string_view::npos ? std::string_view() : content.substr(end + 1);
if (!line.starts_with(key))
continue;
size_t first = line.find('"'), last = line.rfind('"');
if (first == std::string_view::npos || last <= first)
return std::nullopt;
return std::string(line.substr(first + 1, last - first - 1));
}
return std::nullopt;
}
}
+120
View File
@@ -0,0 +1,120 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// First-run install: copy the user's disc (an image or an extracted folder)
// into a game directory, checking every file against the manifest's size and
// SHA-256 as it is copied (one read of the source), so a bad dump is caught
// at install time rather than as a crash an hour into the game.
//
// The copy goes to a sibling "<destination>.partial" directory and is renamed
// into place only once every file has matched and the install marker is
// written, so the destination is never half-installed: a cancelled, failed or
// interrupted install leaves the previous state (nothing, or the old install).
//
// Everything here blocks; a UI runs it on a worker thread. Typical flow:
// OpenDiscSource(path) -> CheckSource() (fast: default.xex + file list;
// show the result) -> CheckDestination() (instant; the confirm page) ->
// Install() (minutes; progress + cancel).
#pragma once
#include "disc_source.h"
#include "manifest.h"
#include <atomic>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
namespace install
{
struct Progress
{
enum class Phase
{
Checking, // default.xex and the file list
Copying, // Install: copying and hashing
Verifying, // Verify: hashing only
Finalizing, // writing the marker, moving the install into place
};
Phase phase = Phase::Checking;
uint64_t bytesDone = 0, bytesTotal = 0;
size_t filesDone = 0, filesTotal = 0;
std::string_view currentFile; // disc-relative path, manifest spelling
};
// Called on the installing thread, after every chunk (a few MB), so keep
// it cheap: store the numbers and let the UI thread draw them.
using ProgressCallback = std::function<void(const Progress&)>;
// Is this the supported game and version? Hashes default.xex (4 MB) and
// reads its title ID: WrongGame (another title, or an original Xbox disc),
// UnsupportedVersion (Most Wanted, another default.xex; foundHash says
// which), NotADiscImage (no default.xex), ReadError. `identity`, if given,
// receives what was found.
Result CheckGameVersion(const DiscSource& source, const Manifest& manifest = GameManifest(),
XexIdentity* identity = nullptr);
// Everything short of hashing the data: CheckGameVersion, then every
// manifest file present with the right size (MissingFile/CorruptFile name
// the first bad file and count the rest), and not cut off (TruncatedImage).
// Files the manifest doesn't list ($SystemUpdate/, ...) are ignored.
Result CheckSource(const DiscSource& source, const Manifest& manifest = GameManifest());
// Copies the manifest's files from `source` into `destination`, which must
// not exist, be empty (hidden files such as .DS_Store aside), or hold a
// previous install. A previous install is replaced only after the new one
// is complete, and only its game files go: everything else in it (dlc/,
// a disc image, the player's own files, even beside the game's in NFS/)
// is moved into the new install. Runs CheckDestination's checks first,
// then holds a lock on the destination's parent folder, so a second
// Install into the same place fails (DestinationNotWritable) instead of
// colliding. Stops between chunks when `cancel` becomes true (Cancelled;
// the partial copy is deleted).
Result Install(const DiscSource& source, const std::filesystem::path& destination,
const ProgressCallback& progress = {}, const std::atomic<bool>* cancel = nullptr,
const Manifest& manifest = GameManifest());
struct DestinationInfo
{
std::filesystem::path path; // the game folder: absolute, links resolved
uint64_t required = 0; // free space Install needs (the files plus a margin)
uint64_t available = UINT64_MAX; // free space there now; UINT64_MAX if it can't be told
bool replacing = false; // a previous install is there (replaced when the new one is done)
};
// Whether Install could use `destination`, without changing anything on
// disk, so a confirm page can show the problem (or the space needed and
// free) before the Install button is pressed: DestinationNotEmpty (other
// files there, a file, or a leftover .partial/.previous that isn't the
// installer's), DestinationNotWritable (no permission, a file in the path,
// the top of a drive, a link to something missing), NotEnoughSpace.
// `info`, if given, is filled as far as the checks got. Install repeats
// these checks itself.
Result CheckDestination(const std::filesystem::path& destination, DestinationInfo* info = nullptr,
const Manifest& manifest = GameManifest());
// Finishes a reinstall that was interrupted (a crash or power cut) while
// the old install sat at "<destination>.previous": puts it back if nothing
// took its place, or retires it (carrying the player's files over) if the
// new install made it. Install and FindGameInstall call it; one stat when
// there is nothing to do, and a no-op while an Install is running there.
void RecoverInterruptedInstall(const std::filesystem::path& destination, const Manifest& manifest = GameManifest());
// Hashes every manifest file in `source` without copying: checks an image
// before installing, or an installed folder ("verify game files").
Result Verify(const DiscSource& source, const ProgressCallback& progress = {},
const std::atomic<bool>* cancel = nullptr, const Manifest& manifest = GameManifest());
// Where Install puts the game by default: GetUserPath()/game.
std::filesystem::path DefaultInstallPath();
// The marker Install writes into the game directory last: it records the
// manifest version the files were checked against. A dot file, so file
// managers hide it; the guest would still see it in a "*" listing of the
// disc root (NtQueryDirectoryFile doesn't filter), which is harmless.
inline constexpr const char* kInstallMarker = ".nfsmw-install";
// The manifest version recorded in `dir`'s marker, if there is one.
std::optional<std::string> ReadInstallMarker(const std::filesystem::path& dir);
}
+376
View File
@@ -0,0 +1,376 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See locate.h.
#include "locate.h"
#include "installer.h"
#include <user/paths.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <set>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#ifdef __APPLE__
#include <sys/mount.h>
#endif
#include <unistd.h>
namespace install
{
namespace
{
namespace fs = std::filesystem;
bool Hidden(const fs::path& path)
{
std::string name = path.filename().string();
return !name.empty() && name[0] == '.';
}
#ifndef __APPLE__
std::string UserName()
{
if (const char* user = std::getenv("USER"); user && *user)
return user;
if (const passwd* pw = getpwuid(getuid()))
return pw->pw_name;
return {};
}
#endif
// default.xex in `dir` with the manifest's size (and, if asked, the
// current marker beside it), as spelled on disk.
std::optional<fs::path> UsableXex(const fs::path& dir, bool requireMarker, const Manifest& manifest)
{
std::error_code ec;
fs::path xex = dir / "default.xex";
if (!fs::is_regular_file(xex, ec))
{
// DEFAULT.XEX from a tool that upper-cases names, on a
// case-sensitive file system.
xex.clear();
std::error_code listEc;
for (fs::directory_iterator it(dir, listEc), end; !listEc && it != end; it.increment(listEc))
{
std::error_code fileEc;
if (PathEqualsIgnoreCase(it->path().filename().string(), "default.xex") && it->is_regular_file(fileEc))
{
xex = it->path();
break;
}
}
if (xex.empty())
return std::nullopt;
}
uint64_t size = fs::file_size(xex, ec);
if (ec || size != manifest.Xex().size)
return std::nullopt;
if (requireMarker && ReadInstallMarker(dir) != std::optional<std::string>(manifest.version))
return std::nullopt;
return xex;
}
GameInstall Found(const fs::path& dir, const fs::path& xex, InstallOrigin origin, const Manifest& manifest)
{
std::error_code ec;
GameInstall install;
fs::path absolute = fs::absolute(dir, ec);
install.path = (ec ? dir : absolute).lexically_normal();
install.xex = install.path / xex.filename();
install.origin = origin;
install.marker = ReadInstallMarker(dir) == std::optional<std::string>(manifest.version);
return install;
}
void Consider(const fs::path& file, uint64_t minSize, const Manifest& manifest,
std::set<fs::path>& seen, std::vector<DiscImageCandidate>& found)
{
std::string extension = file.extension().string();
if (!PathEqualsIgnoreCase(extension, ".iso"))
return;
std::error_code ec;
uint64_t size = fs::file_size(file, ec);
if (ec || size < minSize)
return;
fs::path key = fs::weakly_canonical(file, ec);
if (!seen.insert(ec ? file : key).second)
return;
// Only XDVDFS images whose tree reads count; a truncated one is kept
// (with its status) so the UI can say why it won't install rather
// than leave the user wondering where their image went.
std::unique_ptr<DiscSource> source;
Result opened = OpenDiscSource(file, source, { .allowTruncated = true });
if (!source || !source->Find("default.xex"))
return;
DiscImageCandidate candidate;
candidate.path = file;
candidate.size = size;
XexIdentity identity;
Result checked = CheckGameVersion(*source, manifest, &identity);
candidate.titleId = identity.titleId;
candidate.xexMatches = checked.Ok();
candidate.status = opened.Ok() ? checked : opened;
found.push_back(std::move(candidate));
}
}
bool IsUsableInstall(const std::filesystem::path& dir, bool requireMarker, const Manifest& manifest)
{
return UsableXex(dir, requireMarker, manifest).has_value();
}
std::optional<GameInstall> FindGameInstall(const Manifest& manifest)
{
if (const char* env = std::getenv("NFSMW_GAME_DIR"); env && *env)
{
if (std::optional<fs::path> xex = UsableXex(env, false, manifest))
return Found(env, *xex, InstallOrigin::Environment, manifest);
fprintf(stderr, "[install] NFSMW_GAME_DIR=%s has no default.xex of %llu bytes; ignoring it\n", env,
(unsigned long long)manifest.Xex().size);
}
if (std::optional<fs::path> xex = UsableXex("game/files", false, manifest))
return Found("game/files", *xex, InstallOrigin::Development, manifest);
// An install the player put on another drive.
if (std::optional<fs::path> recorded = RecordedInstallPath())
{
RecoverInterruptedInstall(*recorded, manifest);
if (std::optional<fs::path> xex = UsableXex(*recorded, true, manifest))
return Found(*recorded, *xex, InstallOrigin::User, manifest);
fprintf(stderr, "[install] the game was installed in %s, which isn't usable now (drive not inserted?)\n",
recorded->c_str());
}
fs::path user = DefaultInstallPath();
// A crash in the middle of a reinstall can leave the old install at
// .previous, with nothing (or the new install) in its place.
RecoverInterruptedInstall(user, manifest);
if (std::optional<fs::path> xex = UsableXex(user, true, manifest))
return Found(user, *xex, InstallOrigin::User, manifest);
std::error_code ec;
if (fs::exists(user / "default.xex", ec))
{
std::optional<std::string> marker = ReadInstallMarker(user);
fprintf(stderr, "[install] %s is not a current install (marker %s, manifest %.*s): reinstall\n",
user.c_str(), marker ? marker->c_str() : "missing", int(manifest.version.size()), manifest.version.data());
}
return std::nullopt;
}
namespace
{
fs::path LocationFile()
{
return GetUserPath() / "game-location";
}
// A folder that is the root of a mounted file system.
bool IsMountPoint(const fs::path& dir)
{
struct stat self{}, parent{};
if (stat(dir.c_str(), &self) != 0 || stat(dir.parent_path().c_str(), &parent) != 0)
return false;
return S_ISDIR(self.st_mode) && self.st_dev != parent.st_dev;
}
}
std::optional<std::filesystem::path> RecordedInstallPath()
{
FILE* f = fopen(LocationFile().c_str(), "rb");
if (!f)
return std::nullopt;
char buffer[4096];
size_t n = fread(buffer, 1, sizeof(buffer) - 1, f);
fclose(f);
std::string text(buffer, n);
while (!text.empty() && (text.back() == '\n' || text.back() == '\r'))
text.pop_back();
if (text.empty() || text.front() != '/')
return std::nullopt;
return fs::path(text);
}
bool RecordInstallPath(const std::filesystem::path& dir)
{
std::error_code ec;
fs::path file = LocationFile();
if (fs::absolute(dir, ec).lexically_normal() == fs::absolute(DefaultInstallPath(), ec).lexically_normal())
{
fs::remove(file, ec);
return !ec;
}
fs::create_directories(file.parent_path(), ec);
fs::path temp = file;
temp += ".tmp";
FILE* f = fopen(temp.c_str(), "wb");
if (!f)
return false;
std::string text = fs::absolute(dir, ec).lexically_normal().string() + "\n";
bool ok = fwrite(text.data(), 1, text.size(), f) == text.size();
ok = fflush(f) == 0 && ok;
fsync(fileno(f));
fclose(f);
if (ok)
fs::rename(temp, file, ec);
return ok && !ec;
}
std::vector<InstallDestination> InstallDestinations()
{
std::vector<InstallDestination> out;
out.push_back({ "Internal storage", DefaultInstallPath(), false });
auto addVolume = [&](const fs::path& volume)
{
if (Hidden(volume) || !IsMountPoint(volume))
return;
// Not read-only drives (a disc), nor the system's hidden volumes
// (macOS Recovery, Preboot, VM).
struct statvfs vfs{};
if (statvfs(volume.c_str(), &vfs) != 0 || (vfs.f_flag & ST_RDONLY))
return;
#ifdef __APPLE__
struct statfs sfs{};
if (statfs(volume.c_str(), &sfs) != 0 || (sfs.f_flags & MNT_DONTBROWSE))
return;
#endif
std::string label = volume.filename().string();
out.push_back({ label.empty() ? volume.string() : label, volume / kUserFolderName / "game", true });
};
std::error_code ec;
#ifdef __APPLE__
for (fs::directory_iterator it("/Volumes", ec), end; !ec && it != end; it.increment(ec))
{
std::error_code sameEc;
if (!fs::equivalent(it->path(), "/", sameEc))
addVolume(it->path());
}
#else
std::string user = UserName();
std::vector<fs::path> parents = { "/run/media", "/media" };
if (!user.empty())
{
parents.push_back(fs::path("/run/media") / user);
parents.push_back(fs::path("/media") / user);
}
for (const fs::path& parent : parents)
for (fs::directory_iterator it(parent, fs::directory_options::skip_permission_denied, ec), end; !ec && it != end; it.increment(ec))
addVolume(it->path());
#endif
return out;
}
std::vector<std::filesystem::path> DiscImageSearchFolders()
{
std::vector<fs::path> folders;
auto add = [&](const fs::path& dir)
{
std::error_code ec;
if (fs::is_directory(dir, ec) && std::find(folders.begin(), folders.end(), dir) == folders.end())
folders.push_back(dir);
};
if (const char* home = std::getenv("HOME"); home && *home)
{
fs::path h = home;
add(h);
for (const char* sub : { "Downloads", "Desktop", "Documents", "Games" })
add(h / sub);
}
#ifdef __APPLE__
// /Volumes also lists the startup disk (a link to /): skip it, a scan of
// the whole system disk is what the folders above avoid.
std::error_code ec;
for (fs::directory_iterator it("/Volumes", ec), end; !ec && it != end; it.increment(ec))
{
std::error_code sameEc;
if (!Hidden(it->path()) && !fs::equivalent(it->path(), "/", sameEc))
add(it->path());
}
#else
// SteamOS mounts SD cards and USB drives at /run/media/<user>/<label>
// (older releases: /run/media/<device>); other distributions use
// /run/media/<user> or /media/<user>, or /media/<label>. Every folder
// inside those is a mount point to search.
auto addChildren = [&](const fs::path& dir)
{
std::error_code ec;
for (fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec), end; !ec && it != end; it.increment(ec))
if (!Hidden(it->path()))
add(it->path());
};
std::string user = UserName();
addChildren("/run/media");
if (!user.empty())
addChildren(fs::path("/run/media") / user);
addChildren("/media");
if (!user.empty())
addChildren(fs::path("/media") / user);
#endif
return folders;
}
std::vector<DiscImageCandidate> FindDiscImages(const std::vector<std::filesystem::path>& folders,
uint64_t minSize, const std::atomic<bool>* cancel, const Manifest& manifest)
{
std::vector<DiscImageCandidate> found;
std::set<fs::path> seenFiles, listedDirs;
fs::path library;
if (const char* home = std::getenv("HOME"); home && *home)
library = fs::path(home) / "Library";
auto cancelled = [&] { return cancel && cancel->load(std::memory_order_relaxed); };
// Lists `dir`'s .iso files (once per directory, however it is reached)
// and, for a search folder, collects its subfolders.
auto list = [&](const fs::path& dir, std::vector<fs::path>* subdirs)
{
std::error_code ec;
fs::path key = fs::weakly_canonical(dir, ec);
bool filesListed = !listedDirs.insert(ec ? dir : key).second;
if (filesListed && !subdirs)
return;
for (fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec), end;
!ec && it != end && !cancelled(); it.increment(ec))
{
const fs::path& path = it->path();
if (Hidden(path))
continue;
std::error_code typeEc;
if (it->is_directory(typeEc))
{
// ~/Library holds no disc images, and its subfolders are
// where macOS privacy prompts come from.
if (subdirs && path != library)
subdirs->push_back(path);
}
else if (!filesListed && it->is_regular_file(typeEc))
{
Consider(path, minSize, manifest, seenFiles, found);
}
}
};
for (const fs::path& folder : folders)
{
std::vector<fs::path> subdirs;
list(folder, &subdirs);
std::sort(subdirs.begin(), subdirs.end());
for (const fs::path& sub : subdirs)
{
if (cancelled())
return found;
list(sub, nullptr);
}
}
std::sort(found.begin(), found.end(), [](const DiscImageCandidate& a, const DiscImageCandidate& b)
{
if (a.status.Ok() != b.status.Ok())
return a.status.Ok();
return a.path < b.path;
});
return found;
}
}
+94
View File
@@ -0,0 +1,94 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// Finding things before any UI question is asked: an installed game (so
// startup can skip the installer), and disc images already on the machine
// (so a controller-only first run can offer "Install from <image>" instead of
// a file browser).
#pragma once
#include "manifest.h"
#include "result.h"
#include <atomic>
#include <filesystem>
#include <optional>
#include <vector>
namespace install
{
enum class InstallOrigin
{
Environment, // NFSMW_GAME_DIR
Development, // ./game/files, extracted by hand in a checkout
User, // GetUserPath()/game, made by Install
};
struct GameInstall
{
std::filesystem::path path; // absolute; the directory holding default.xex
// default.xex itself, as spelled on disk: a hand-extracted folder on
// a case-sensitive file system may hold DEFAULT.XEX, which
// path / "default.xex" would not open. Load the executable from here.
std::filesystem::path xex;
InstallOrigin origin = InstallOrigin::User;
bool marker = false; // holds Install's marker for the current manifest
};
// The first usable game directory of:
// 1. NFSMW_GAME_DIR, if set (skipped with a log line if it isn't usable)
// 2. ./game/files (a development checkout)
// 3. GetUserPath()/game (DefaultInstallPath(), where Install puts it)
// Each needs default.xex with the manifest's size. The third also needs
// Install's marker for the current manifest version: it is the one the
// installer owns, and an old marker means the manifest changed since, so
// reinstall. The first two were set up by hand and are trusted without a
// marker. nullopt: nothing usable, so run the installer.
// Before looking at the third, finishes a reinstall a crash interrupted
// (RecoverInterruptedInstall).
// Cheap: a few stat calls and one small read; no hashing.
std::optional<GameInstall> FindGameInstall(const Manifest& manifest = GameManifest());
// The same check for one directory.
bool IsUsableInstall(const std::filesystem::path& dir, bool requireMarker, const Manifest& manifest = GameManifest());
// Where the player chose to install when it wasn't DefaultInstallPath()
// (an SD card, a USB drive): recorded in GetUserPath()/game-location by
// the installer, and searched by FindGameInstall after NFSMW_GAME_DIR and
// ./game/files. Recording the default path removes the record.
std::optional<std::filesystem::path> RecordedInstallPath();
bool RecordInstallPath(const std::filesystem::path& dir);
struct InstallDestination
{
std::string label; // "Internal storage", or the drive's name
std::filesystem::path path; // the game folder to install into
bool removable = false;
};
// Where the installer offers to put the game: DefaultInstallPath() first,
// then a folder on each mounted removable drive (<drive>/speedbreaker/game; SpeedBreaker on macOS).
std::vector<InstallDestination> InstallDestinations();
struct DiscImageCandidate
{
std::filesystem::path path;
uint64_t size = 0;
bool xexMatches = false; // default.xex is the supported one (Install still checks every file)
uint32_t titleId = 0; // default.xex's title ID (0 if unreadable)
Result status; // Ok, or why it won't install: UnsupportedVersion, WrongGame, TruncatedImage, ReadError
};
// Where FindDiscImages looks by default: the home folder, Downloads,
// Desktop, Documents and Games in it, removable drives and SD cards
// (/run/media/*, /run/media/$USER/*, /media/*, /media/$USER/* on Linux;
// /Volumes/* except the startup disk on macOS). Only existing folders.
std::vector<std::filesystem::path> DiscImageSearchFolders();
// .iso files of at least `minSize` bytes directly in each folder or one
// folder below it, that hold an XDVDFS game partition with a default.xex.
// Each costs a directory read and hashing default.xex (a few MB). Sorted:
// installable images first, then by path. Blocks; run it off the UI thread.
// On macOS, reading Desktop, Documents, Downloads or a removable volume
// makes the system ask the user for permission the first time.
std::vector<DiscImageCandidate> FindDiscImages(const std::vector<std::filesystem::path>& folders = DiscImageSearchFolders(),
uint64_t minSize = 1ull << 30, const std::atomic<bool>* cancel = nullptr, const Manifest& manifest = GameManifest());
}
+86
View File
@@ -0,0 +1,86 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See manifest.h.
#include "manifest.h"
#include <cassert>
#include <iterator>
namespace install
{
namespace
{
#define MANIFEST_INFO(version, titleId, count, bytes)
#define MANIFEST_FILE(path, size, sha256) ManifestFile{ path, size, sha256 },
constexpr ManifestFile s_files[] = {
#include "manifest.inc"
};
#undef MANIFEST_INFO
#undef MANIFEST_FILE
#define MANIFEST_INFO(version, titleId, count, bytes) \
constexpr std::string_view s_version = version; \
constexpr uint32_t s_titleId = titleId; \
constexpr size_t s_count = count; \
constexpr uint64_t s_bytes = bytes;
#define MANIFEST_FILE(path, size, sha256)
#include "manifest.inc"
#undef MANIFEST_INFO
#undef MANIFEST_FILE
constexpr uint64_t SumSizes()
{
uint64_t sum = 0;
for (const ManifestFile& f : s_files)
sum += f.size;
return sum;
}
constexpr bool HasXex()
{
for (const ManifestFile& f : s_files)
if (f.path == "default.xex")
return true;
return false;
}
// A hand-edited or truncated manifest.inc fails the build, not an install.
static_assert(std::size(s_files) == s_count, "manifest.inc: file count does not match MANIFEST_INFO");
static_assert(SumSizes() == s_bytes, "manifest.inc: sizes do not add up to MANIFEST_INFO's total");
static_assert(HasXex(), "manifest.inc: no default.xex entry");
char Lower(char c)
{
return c >= 'A' && c <= 'Z' ? char(c - 'A' + 'a') : c;
}
}
bool PathEqualsIgnoreCase(std::string_view a, std::string_view b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); i++)
if (Lower(a[i]) != Lower(b[i]))
return false;
return true;
}
const ManifestFile& Manifest::Xex() const
{
const ManifestFile* xex = Find("default.xex");
assert(xex && "a manifest must list default.xex");
return *xex;
}
const ManifestFile* Manifest::Find(std::string_view path) const
{
for (const ManifestFile& f : files)
if (PathEqualsIgnoreCase(f.path, path))
return &f;
return nullptr;
}
const Manifest& GameManifest()
{
static const Manifest manifest{ s_version, s_titleId, s_files, s_bytes };
return manifest;
}
}
+40
View File
@@ -0,0 +1,40 @@
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
//
// The known-good disc: every file's disc-relative path, size and SHA-256,
// generated from a verified dump by scripts/gen_install_manifest.py into
// manifest.inc. The recompiled code only works with the one default.xex it was
// generated from, so that file's hash doubles as the supported-version check.
#pragma once
#include <cstdint>
#include <span>
#include <string_view>
namespace install
{
struct ManifestFile
{
std::string_view path; // '/'-separated, in the dump's case (Movies/, NFS/, default.xex)
uint64_t size;
std::string_view sha256; // lower-case hex
};
struct Manifest
{
std::string_view version; // changes whenever the entries do; recorded in the install marker
uint32_t titleId; // from default.xex's execution info (0x454107D9)
std::span<const ManifestFile> files;
uint64_t totalBytes;
// The default.xex entry (the one file every manifest must have).
const ManifestFile& Xex() const;
// Case-insensitive; nullptr if absent.
const ManifestFile* Find(std::string_view path) const;
};
// The manifest compiled in from manifest.inc. Every API that checks files
// takes a Manifest so tests can pass a small one instead.
const Manifest& GameManifest();
// ASCII case-insensitive equality: the disc's names are ASCII.
bool PathEqualsIgnoreCase(std::string_view a, std::string_view b);
}
Loaded 100 of 290 files, more files were not shown because too many files have changed in this diff. Show more