SpeedBreaker v0.1.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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>.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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>.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
------------------------------------------------------------------------------
|
||||
@@ -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/>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
·
|
||||
<a href="#quick-start">Quick start</a>
|
||||
·
|
||||
<a href="https://projectugarte.com/speedbreaker/">Website</a>
|
||||
·
|
||||
<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
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 656 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 68 KiB |
@@ -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"]
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
)"
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||