Wind Waker (GameCube) for PlayStation 5: SwitchWaker on the PS5 Vulkan stack
Title PPSA99309. The build holds no game data: the player's own GZLE01 disc image is read from the title's folder at start. 16:9, 3840x2160 output, 60 fps by frame interpolation, DualSense symbols on the HUD, shader caches from a console shipped and seeded on a first start. First run on a console 2026-10-12. Claude-Session: https://claude.ai/code/session_018jvMS8Y8okH8SDZUfd8MP1
No files matched your search
@@ -0,0 +1,14 @@
|
||||
.DS_Store
|
||||
build/
|
||||
dist/
|
||||
klog/
|
||||
.deps/
|
||||
gamedata/
|
||||
*.log
|
||||
imgui.ini
|
||||
# never: disc images
|
||||
*.iso
|
||||
*.ciso
|
||||
*.gcm
|
||||
*.rvz
|
||||
*.gci
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "vendor/switchwaker"]
|
||||
path = vendor/switchwaker
|
||||
url = https://github.com/centollOS/SwitchWaker.git
|
||||
@@ -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,42 @@
|
||||
# Licences
|
||||
|
||||
The repository as a whole, and any title built from it, is offered under the GNU
|
||||
General Public License, version 3 ([`LICENSE`](LICENSE)). Its files come from
|
||||
projects under several licences, each compatible with that; every source file
|
||||
names its own in its header, and may also be used under that licence alone.
|
||||
|
||||
| Path | Licence | Text |
|
||||
| --- | --- | --- |
|
||||
| `ps5/tools/png-to-title-dds.py` (encoder from PS5 RetroArch, by Mihawk) | GPL-3.0-or-later | [`licenses/GPL-3.0.txt`](licenses/GPL-3.0.txt) |
|
||||
| `ps5/src/platform.c`, `ps5/src/platform.h` and the tools taken from the PS5 Vulkan Template (by Mihawk) | MIT | [`licenses/MIT.txt`](licenses/MIT.txt) |
|
||||
| The rest of `ps5/src/`, `ps5/tools/`, `ps5/cmake/`, `ps5/art/*.py`, `ps5/art/*.sh` and `ps5/nod/` (written for this port and for PS5_Dusklight) | MIT | [`licenses/MIT.txt`](licenses/MIT.txt) |
|
||||
| `ps5/sdl/` (SDL3 drivers for the console) and `ps5/patches/sdl.patch` | Zlib, as SDL | [`licenses/Zlib.txt`](licenses/Zlib.txt) |
|
||||
| `ps5/patches/dawn.patch` | BSD-3-Clause, as Dawn | in Dawn's source |
|
||||
| `ps5/patches/dawn-abseil.patch` | Apache-2.0, as Abseil | in Abseil's source |
|
||||
| `ps5/patches/aurora.patch` | MIT, as Aurora | in Aurora's source |
|
||||
| `ps5/patches/switchwaker.patch` | MIT, as SwitchWaker | in SwitchWaker's source |
|
||||
| `ps5/art/kenney/` (Kenney's Input Prompts) | CC0-1.0 | `ps5/art/kenney/License.txt` |
|
||||
| `ps5/art/glyphs/`, the icon and the background | made for this port | |
|
||||
| `ps5/shader-cache/` | compiled output of this build's shader generator and driver, recorded on a console; no game data | |
|
||||
|
||||
## What is built from elsewhere
|
||||
|
||||
Nothing below is stored in this repository; the build takes each from its own
|
||||
project, under that project's licence.
|
||||
|
||||
| Part | Project | Licence |
|
||||
| --- | --- | --- |
|
||||
| The game's code | [SwitchWaker](https://github.com/centollOS/SwitchWaker) (MIT), which compiles the [Wind Waker decompilation](https://github.com/zeldaret/tww); part of it comes from an AI-assisted fork (see SwitchWaker's README) | MIT; CC0-1.0 |
|
||||
| Sound | Dolphin's DSP HLE, as SwitchWaker uses it | GPL-2.0-or-later |
|
||||
| The GameCube layer | [Aurora](https://github.com/encounter/aurora) | MIT |
|
||||
| WebGPU | Dawn, at the revision Aurora pins | BSD-3-Clause |
|
||||
| Windowing, input and sound output | [SDL3](https://github.com/libsdl-org/SDL) | Zlib |
|
||||
| The Vulkan driver | RADV from [PS5_Mesa](https://github.com/mihawk-99/PS5_Mesa), linked by [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan) | MIT (Mesa); GPL-3.0 (PS5_Vulkan's tools) |
|
||||
| The platform layer and SDK | [PS5_PayloadSDK](https://github.com/mihawk-99/PS5_PayloadSDK) | GPL-3.0 |
|
||||
|
||||
## A built title
|
||||
|
||||
A built `eboot.bin` links the GPL-3.0 platform layer and Dolphin's
|
||||
GPL-2.0-or-later DSP HLE with everything above, so a title as distributed is
|
||||
under **GPL-3.0**. It holds no game data: the player supplies their own disc
|
||||
image. This has not been reviewed for a release; no release has been made.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Wind Waker (GameCube) for PlayStation 5
|
||||
|
||||
A PS5 homebrew port of [SwitchWaker](https://github.com/centollOS/SwitchWaker), the
|
||||
native port of the 2002 GameCube game (USA disc `GZLE01`, revision 0) compiled
|
||||
from its decompilation and drawn with [Aurora](https://github.com/encounter/aurora).
|
||||
It is laid out like [PS5_Dusklight](https://github.com/griches/PS5_Dusklight) and
|
||||
uses the same console layer: Aurora on Dawn's WebGPU on RADV, from the
|
||||
[PS5 Vulkan](https://github.com/mihawk-99/PS5_Vulkan) stack.
|
||||
|
||||
The build reads no disc and holds no game data: SwitchWaker's runtime-assets
|
||||
mode compiles against stub headers and fills them from the player's own disc
|
||||
image when the title starts.
|
||||
|
||||
## Status
|
||||
|
||||
**Runs on the console.** First run on 2026-10-12 on a PS5 at 3840x2160: the
|
||||
title screen, name entry, the prologue and Outset, at 60 images a second
|
||||
(frame interpolation; the game's logic stays at 30), with the DualSense's
|
||||
symbols on the HUD. Played for minutes, not hours: nothing past Outset has been
|
||||
seen on the console.
|
||||
|
||||
No release yet. A release will be the title's folder with no game in it: the
|
||||
player copies their own `GZLE01` disc image (`.iso`, `.gcm` or `.ciso`) into
|
||||
`/data/homebrew/PPSA99309/` beside `eboot.bin`, and the title finds it (the
|
||||
first one in its folder, then on a USB drive).
|
||||
|
||||
| Area | State |
|
||||
| --- | --- |
|
||||
| Disc | Found in the title's folder; a `.iso` was used on the console, `.ciso` only on the Mac build. A missing or wrong disc exits with a line in klog and no message on screen |
|
||||
| Picture | 16:9. The output is 3840x2160 unless the options menu's "Output resolution" says 2560x1440 or 1920x1080 (applied at the next start); the console scales it to the television. "Internal resolution" follows it ("Window size") or is fixed, which dynamic resolution needs |
|
||||
| 60 fps | Held at 3840x2160 in the scenes above, by the frame counter on screen. Not measured over a long session |
|
||||
| Shaders | A first start with no cache compiled 3,056 render pipelines in about 12 minutes. The title now ships the caches a console produced (`ps5/shader-cache`) and copies them into place when the console has none: the same start then compiles nothing. They must be taken from a console again after a change to Dawn, the driver or the shaders |
|
||||
| Button prompts | The HUD shows Cross, Circle, Square, Triangle, R1 for Z, R2 for R and a right stick for the C stick (`ps5/art/make-glyphs.py`, Kenney's CC0 Input Prompts). Not done: the button pictures inside message text (they are in the game's font), and the options menu's own hints, which still name the GameCube's buttons |
|
||||
| Options menu | The touch pad's click opens it |
|
||||
| Saves, settings | `/data/homebrew/PPSA99309/user/`; the first start asks to create a save file, as the game does on an empty memory card |
|
||||
| HD textures | SwitchWaker's replacement of textures from a Dolphin pack the player supplies (`user/hd_textures`, off by default) is built in; not yet seen working on the console |
|
||||
| Sound | Not checked by ear in this log |
|
||||
| DevLoop | `devloop-shot` and `devloop-quit` in the title's folder (registered as `windwaker-gc`); the shot answers once the game's loop runs |
|
||||
| Art | The icon and background are the port's own, cel-shaded, made with an image generator (`ps5/art/`) |
|
||||
|
||||
Controls: left stick to move, right stick for the camera (the C stick), Cross A,
|
||||
Circle B, Square X, Triangle Y, R1 Z, L2 L, R2 R, Options START. The mapping is
|
||||
by name, as PS5_Dusklight's, which on the HUD puts Square to the right of Cross
|
||||
and Circle to its lower left, not where they sit on the pad.
|
||||
|
||||
To try a change without the console:
|
||||
|
||||
```sh
|
||||
cmake -S vendor/switchwaker/native -B build/native-mac -G Ninja \
|
||||
-DFETCHCONTENT_SOURCE_DIR_AURORA=$PWD/.deps/aurora -DCOS_AURORA_PREPATCHED=ON \
|
||||
-DCOS_RECOMPCORE_DIR=$PWD/.deps/recompcore \
|
||||
-DCOS_RUNTIME_ASSETS=ON -DCOS_ASSETS_DIR=$PWD/.deps/assets/GZLE01
|
||||
ninja -C build/native-mac switchwaker
|
||||
COS_BUILD_DIR=$PWD/build/native-mac COS_DISC=/path/to/GZLE01.iso COS_FPS60=1 \
|
||||
COS_BUTTON_GLYPHS=$PWD/ps5/art/glyphs vendor/switchwaker/native/tools/run.sh run \
|
||||
--preset sailing --aspect 16:9 --frames 700 --shot 690
|
||||
```
|
||||
|
||||
`COS_TEXTURE_DUMPS=1` writes every texture drawn into the cache folder's
|
||||
`texture_dumps`, named as a replacement for it must be named.
|
||||
|
||||
## Build
|
||||
|
||||
As PS5_Dusklight: on Linux (on a Mac, in the Lima VM) with PS5_Vulkan, PS5_Mesa
|
||||
and PS5_PayloadSDK beside this folder.
|
||||
|
||||
```sh
|
||||
ps5/tools/vm.sh build # from macOS; ps5/tools/build.sh inside the VM
|
||||
ps5/tools/vm.sh deploy
|
||||
ps5/tools/vm.sh run
|
||||
```
|
||||
|
||||
`build.sh` runs `setup-source.sh` first, which patches `vendor/switchwaker`
|
||||
(`ps5/patches/switchwaker.patch`, every hunk behind `__PROSPERO__`) and fetches
|
||||
into `.deps/` Aurora at SwitchWaker's pin with its patches, Dolphin's DSP HLE
|
||||
sources and the stub asset headers. While changing `vendor/switchwaker`, run
|
||||
`git -C vendor/switchwaker diff > ps5/patches/switchwaker.patch` before a build.
|
||||
|
||||
## Licence
|
||||
|
||||
GPL-3.0 for the repository as a whole and for a built title: see
|
||||
[`LICENSING.md`](LICENSING.md). No game data is stored here or built in.
|
||||
@@ -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,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Sascha Willems
|
||||
|
||||
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,17 @@
|
||||
zlib License
|
||||
|
||||
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.
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 281 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1,28 @@
|
||||
|
||||
|
||||
Input Prompts (1.5A)
|
||||
|
||||
Created/distributed by Kenney (www.kenney.nl)
|
||||
Creation date: 11-07-2026
|
||||
|
||||
------------------------------
|
||||
|
||||
License: (Creative Commons Zero, CC0)
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
You can use this content for personal, educational, and commercial purposes.
|
||||
|
||||
Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement)
|
||||
|
||||
------------------------------
|
||||
|
||||
• Website : www.kenney.nl
|
||||
• Donate : www.kenney.nl/donate
|
||||
|
||||
• Patreon : patreon.com/kenney
|
||||
|
||||
Follow on social media for updates:
|
||||
|
||||
• Twitter: twitter.com/KenneyNL
|
||||
• Instagram: instagram.com/kenney_nl
|
||||
• Mastodon: mastodon.gamedev.place/@kenney
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 978 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 844 B |
|
After Width: | Height: | Size: 810 B |
|
After Width: | Height: | Size: 919 B |
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
# Wind Waker for the PlayStation 5 - the DualSense's symbols for the game's button prompts.
|
||||
# SPDX-License-Identifier: MIT
|
||||
"""Makes ps5/art/glyphs/*.png: replacement textures for the game's button prompts.
|
||||
|
||||
The game draws a button as a coloured shape with a white letter over it, each letter a small
|
||||
texture of its own. Aurora replaces a texture by a file named after it (Dolphin's scheme:
|
||||
tex1_<width>x<height>_<hash of its texels>[_<hash of its palette>]_<format>.png), so each
|
||||
picture here carries the name of what it stands in for and nothing of it: a cross for A, a
|
||||
circle for B, a square for X and a triangle for Y, as the default mapping has them (A south,
|
||||
B east, X west, Y north), "R2" for R, and for Z, whose button is one picture with its letter,
|
||||
a button drawn here that says "R1". The C stick's picture (a yellow knob beside an eye) becomes
|
||||
Kenney's right stick beside an eye drawn here. The A button's green face becomes a blue one, drawn here:
|
||||
a cross belongs on blue. The names are the GameCube USA disc's (GZLE01), read from a texture
|
||||
dump (COS_TEXTURE_DUMPS=1).
|
||||
|
||||
The symbols are Kenney's Input Prompts (www.kenney.nl, CC0: ps5/art/kenney/): of each of its
|
||||
outlined buttons the outline is dropped, since the game draws the button itself, and what is
|
||||
inside is kept.
|
||||
|
||||
Needs Python with Pillow.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
SCALE = 4 # the replacement is this many times the original's size
|
||||
art = Path(__file__).resolve().parent
|
||||
out = art / "glyphs"
|
||||
out.mkdir(exist_ok=True)
|
||||
for old in out.glob("*.png"):
|
||||
old.unlink()
|
||||
|
||||
|
||||
def inside(source):
|
||||
"""The picture without its outline: the connected shape with the largest bounds goes."""
|
||||
alpha = Image.open(art / "kenney" / (source + ".png")).convert("RGBA").getchannel("A")
|
||||
w, h = alpha.size
|
||||
values = list(alpha.tobytes())
|
||||
solid = [v > 40 for v in values]
|
||||
label = [0] * (w * h)
|
||||
shapes = [] # (bounds' area, label)
|
||||
for start in range(w * h):
|
||||
if not solid[start] or label[start]:
|
||||
continue
|
||||
n = len(shapes) + 1
|
||||
label[start] = n
|
||||
stack = [start]
|
||||
x0 = x1 = start % w
|
||||
y0 = y1 = start // w
|
||||
while stack:
|
||||
p = stack.pop()
|
||||
x, y = p % w, p // w
|
||||
x0, x1, y0, y1 = min(x0, x), max(x1, x), min(y0, y), max(y1, y)
|
||||
for q in (p - 1 if x else -1, p + 1 if x < w - 1 else -1, p - w, p + w):
|
||||
if 0 <= q < w * h and solid[q] and not label[q]:
|
||||
label[q] = n
|
||||
stack.append(q)
|
||||
shapes.append(((x1 - x0) * (y1 - y0), n))
|
||||
outline = max(shapes)[1]
|
||||
keep = Image.frombytes("L", (w, h), bytes(a if solid[i] and label[i] != outline else 0 for i, a in enumerate(values)))
|
||||
return keep.crop(keep.getbbox())
|
||||
|
||||
|
||||
def glyph(name, w, h, source, fill=0.8):
|
||||
symbol = inside(source)
|
||||
box = (w * SCALE * fill, h * SCALE * fill)
|
||||
k = min(box[0] / symbol.width, box[1] / symbol.height)
|
||||
symbol = symbol.resize((max(1, round(symbol.width * k)), max(1, round(symbol.height * k))), Image.LANCZOS)
|
||||
mask = Image.new("L", (w * SCALE, h * SCALE), 0)
|
||||
mask.paste(symbol, ((mask.width - symbol.width) // 2, (mask.height - symbol.height) // 2))
|
||||
# the game draws these about 30 pixels high at 1080p: Kenney's strokes are thin for that
|
||||
mask = mask.filter(ImageFilter.MaxFilter(5))
|
||||
white = Image.new("RGBA", mask.size, (255, 255, 255, 0))
|
||||
white.putalpha(mask)
|
||||
white.save(out / (name + ".png"))
|
||||
|
||||
|
||||
def fitted(source, w, h):
|
||||
symbol = inside(source)
|
||||
k = min(w / symbol.width, h / symbol.height)
|
||||
return symbol.resize((max(1, round(symbol.width * k)), max(1, round(symbol.height * k))), Image.LANCZOS)
|
||||
|
||||
|
||||
def z_button(name, w, h):
|
||||
"""The Z button whole: a rounded purple button with a light rim, and R1 on it."""
|
||||
W, H = w * SCALE, h * SCALE
|
||||
picture = Image.new("RGBA", (W * 2, H * 2), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(picture)
|
||||
draw.rounded_rectangle([6, 14, W * 2 - 7, H * 2 - 15], radius=40, fill=(226, 208, 255, 255))
|
||||
draw.rounded_rectangle([18, 26, W * 2 - 19, H * 2 - 27], radius=30, fill=(126, 62, 214, 255))
|
||||
picture = picture.resize((W, H), Image.LANCZOS)
|
||||
symbol = fitted("playstation_trigger_r1_outline", W * 0.62, H * 0.5)
|
||||
white = Image.new("RGBA", symbol.size, (255, 255, 255, 255))
|
||||
picture.paste(white, ((W - symbol.width) // 2, (H - symbol.height) // 2), symbol)
|
||||
picture.save(out / (name + ".png"))
|
||||
|
||||
|
||||
def face(name, size, colour, rim):
|
||||
"""A button's round face: a flat disc with a lighter rim, as the game's are shaped."""
|
||||
S = size * SCALE
|
||||
picture = Image.new("RGBA", (S * 2, S * 2), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(picture)
|
||||
draw.ellipse([4, 4, S * 2 - 5, S * 2 - 5], fill=rim + (255,))
|
||||
draw.ellipse([20, 20, S * 2 - 21, S * 2 - 21], fill=colour + (255,))
|
||||
picture.resize((S, S), Image.LANCZOS).save(out / (name + ".png"))
|
||||
|
||||
|
||||
def stick_and_eye(name, w, h):
|
||||
"""The C stick's prompt, 128x32: a knob at x 27..52 and an eye at x 55..95, both y 0..25."""
|
||||
picture = Image.new("RGBA", (w * SCALE * 2, h * SCALE * 2), (0, 0, 0, 0))
|
||||
k = SCALE * 2
|
||||
stick = Image.open(art / "kenney" / "playstation_stick_r.png").convert("RGBA")
|
||||
stick = stick.crop(stick.getbbox()).resize((26 * k, 26 * k), Image.LANCZOS)
|
||||
# dark behind it, as the game's own has: the prompt sits over sky and sea
|
||||
shadow = Image.new("RGBA", stick.size, (20, 20, 30, 255))
|
||||
back = stick.getchannel("A").filter(ImageFilter.MaxFilter(9))
|
||||
picture.paste(shadow, (27 * k, 0), back)
|
||||
picture.alpha_composite(stick, (27 * k, 0))
|
||||
draw = ImageDraw.Draw(picture)
|
||||
cx, cy = 75 * k, 13 * k
|
||||
rx, ry = 20 * k, 10 * k
|
||||
eye = [(cx - rx, cy), (cx - rx // 2, cy - ry * 8 // 10), (cx, cy - ry), (cx + rx // 2, cy - ry * 8 // 10),
|
||||
(cx + rx, cy), (cx + rx // 2, cy + ry * 8 // 10), (cx, cy + ry), (cx - rx // 2, cy + ry * 8 // 10)]
|
||||
draw.polygon(eye, fill=(255, 255, 255, 255), outline=(20, 20, 30, 255), width=k)
|
||||
draw.ellipse([cx - 7 * k, cy - 7 * k, cx + 7 * k, cy + 7 * k], fill=(20, 20, 30, 255))
|
||||
picture.resize((w * SCALE, h * SCALE), Image.LANCZOS).save(out / (name + ".png"))
|
||||
|
||||
|
||||
glyph("tex1_16x16_f2ecddd16b5f554a_0", 16, 16, "playstation_button_cross_outline") # A
|
||||
glyph("tex1_16x16_0bbde935468955d4_0", 16, 16, "playstation_button_circle_outline") # B
|
||||
glyph("tex1_16x16_a954694dda754d44_0", 16, 16, "playstation_button_square_outline") # X
|
||||
glyph("tex1_16x16_c78319a187997148_0", 16, 16, "playstation_button_triangle_outline") # Y
|
||||
glyph("tex1_16x16_fd8f1a5a26d95e59_0", 16, 16, "playstation_trigger_r2_outline", 1.0) # R
|
||||
z_button("tex1_48x24_c07e25b9bca86b7e_ce3f64d5da874475_8", 48, 24) # Z
|
||||
face("tex1_40x40_08daf72f6acc9478_1acd6c849fcbc8cc_9", 40, (58, 110, 226), (170, 200, 255)) # A's face
|
||||
stick_and_eye("tex1_128x32_523124ff1f96d4bc_e7820c87ebbc4db4_9", 128, 32) # C stick
|
||||
print(sorted(p.name for p in out.glob("*.png")))
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - the title's art, from the pictures in this folder.
|
||||
#
|
||||
# ps5/art/make-title-art.sh
|
||||
#
|
||||
# icon0-source.png -> ps5/sce_sys/icon0.png the home-screen tile, 512x512
|
||||
# pic-source.png -> ps5/sce_sys/pic0.dds the background behind the title, 3840x2160
|
||||
# -> ps5/sce_sys/pic1.dds the splash while it starts
|
||||
#
|
||||
# Both pictures are the port's own (a sailing boat on a cel-shaded sea, made for it
|
||||
# by Gary Riches with an image generator): nothing in them
|
||||
# is from the game.
|
||||
# Needs Python with Pillow and numpy.
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
art=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
ps5=$(dirname "$art")
|
||||
python3 - "$art/icon0-source.png" "$ps5/sce_sys/icon0.png" <<'PY'
|
||||
import sys
|
||||
from PIL import Image
|
||||
Image.open(sys.argv[1]).convert("RGB").resize((512, 512), Image.LANCZOS).save(sys.argv[2])
|
||||
PY
|
||||
python3 "$ps5/tools/png-to-title-dds.py" "$art/pic-source.png" "$ps5/sce_sys/pic0.dds"
|
||||
cp "$ps5/sce_sys/pic0.dds" "$ps5/sce_sys/pic1.dds"
|
||||
ls -l "$ps5/sce_sys"
|
||||
|
After Width: | Height: | Size: 2.1 MiB |
@@ -0,0 +1,124 @@
|
||||
# Wind Waker for the PlayStation 5 - what the console adds to SwitchWaker's own build.
|
||||
#
|
||||
# tools/build.sh configures vendor/switchwaker/native itself with the payload
|
||||
# SDK's toolchain, and names this file as CMAKE_PROJECT_cos_native_INCLUDE:
|
||||
# CMake includes it right after SwitchWaker's project() call, before Aurora
|
||||
# looks for its dependencies. It
|
||||
# provides the three Aurora cannot fetch for the console:
|
||||
#
|
||||
# Dawn .deps/dawn, patched (tools/setup-dawn.sh): Vulkan only, on RADV
|
||||
# SDL3 .deps/SDL, patched (tools/setup-sdl.sh), with the drivers in ps5/sdl
|
||||
# nod ps5/nod: a plain GameCube .iso reader with nod's C API
|
||||
#
|
||||
# and makes the executable's link PS5_Vulkan's (tools/link-title.sh, with RADV).
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
foreach(required PS5_PORT_DIR PS5_VULKAN_DIR DAWN_DIR SDL_DIR)
|
||||
if(NOT ${required})
|
||||
message(FATAL_ERROR "ps5-game.cmake: ${required} is not set (use ps5/tools/build.sh)")
|
||||
endif()
|
||||
endforeach()
|
||||
set(ps5 ${PS5_PORT_DIR}/ps5)
|
||||
|
||||
# -fno-omit-frame-pointer: the console's crash backtraces are frame-pointer walks.
|
||||
add_compile_options(-g1 -ffunction-sections -fdata-sections -fno-omit-frame-pointer)
|
||||
# An init array, in the order the source has: tools/cc-init-array.sh has the reason
|
||||
set(CMAKE_C_COMPILER_LAUNCHER sh ${ps5}/tools/cc-init-array.sh)
|
||||
set(CMAKE_CXX_COMPILER_LAUNCHER sh ${ps5}/tools/cc-init-array.sh)
|
||||
# The SDK's libc++ is LLVM 18's, where std::jthread and std::stop_token (Aurora's
|
||||
# threads) are still experimental: tools/link-title.sh links libc++experimental.a
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fexperimental-library>)
|
||||
|
||||
# The console: klog, the pad, the title's exit, and the libc names a title cannot import
|
||||
add_library(ps5_platform STATIC ${ps5}/src/platform.c ${ps5}/src/shims.c ${ps5}/src/startup.c)
|
||||
target_include_directories(ps5_platform PUBLIC ${ps5}/src)
|
||||
target_include_directories(ps5_platform PRIVATE ${SDL_DIR}/include)
|
||||
file(READ ${ps5}/sce_sys/param.json param_json)
|
||||
string(JSON PS5_TITLE_NAME GET "${param_json}" localizedParameters en-US titleName)
|
||||
target_compile_definitions(ps5_platform PRIVATE "PS5_TITLE_NAME=\"${PS5_TITLE_NAME}\"")
|
||||
set_target_properties(ps5_platform PROPERTIES ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
|
||||
# By its file, not its target: it is linked whole into the title (below), and a
|
||||
# target here would have to be in every dependency's export set
|
||||
set(ps5_platform_archive ${CMAKE_BINARY_DIR}/libps5_platform.a)
|
||||
|
||||
# Dawn: the Vulkan backend alone, as one static library. No window system:
|
||||
# the surface is VK_KHR_display.
|
||||
set(DAWN_PLATFORM_PS5 ON CACHE INTERNAL "")
|
||||
set(DAWN_FETCH_DEPENDENCIES OFF CACHE INTERNAL "")
|
||||
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC CACHE INTERNAL "")
|
||||
set(DAWN_ENABLE_VULKAN ON CACHE INTERNAL "")
|
||||
foreach(off DAWN_ENABLE_D3D11 DAWN_ENABLE_D3D12 DAWN_ENABLE_METAL DAWN_ENABLE_NULL
|
||||
DAWN_ENABLE_DESKTOP_GL DAWN_ENABLE_OPENGLES DAWN_ENABLE_WEBGPU_ON_WEBGPU
|
||||
DAWN_USE_X11 DAWN_USE_WAYLAND DAWN_USE_GLFW DAWN_BUILD_SAMPLES DAWN_BUILD_TESTS
|
||||
DAWN_BUILD_BENCHMARKS DAWN_BUILD_PROTOBUF DAWN_ENABLE_INSTALL DAWN_WERROR
|
||||
DAWN_ENABLE_SPIRV_VALIDATION
|
||||
TINT_BUILD_CMD_TOOLS TINT_BUILD_TESTS TINT_BUILD_BENCHMARKS TINT_BUILD_IR_BINARY
|
||||
TINT_BUILD_GLSL_WRITER TINT_BUILD_GLSL_VALIDATOR TINT_BUILD_HLSL_WRITER
|
||||
TINT_BUILD_MSL_WRITER TINT_BUILD_NULL_WRITER TINT_BUILD_SPV_READER TINT_BUILD_TINTD)
|
||||
set(${off} OFF CACHE INTERNAL "")
|
||||
endforeach()
|
||||
add_subdirectory(${DAWN_DIR} ${CMAKE_BINARY_DIR}/dawn EXCLUDE_FROM_ALL)
|
||||
set(AURORA_DAWN_PROVIDER vendor CACHE STRING "" FORCE)
|
||||
set(AURORA_DAWN_LINKAGE static CACHE STRING "" FORCE)
|
||||
|
||||
# SDL3: threads, time and files as a FreeBSD has them; video, audio and the pad from ps5/sdl
|
||||
set(SDL_PS5_PORT_DIR ${ps5}/sdl CACHE INTERNAL "")
|
||||
set(SDL_PS5_PLATFORM_DIR ${ps5}/src CACHE INTERNAL "")
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
# no X11 and no Wayland, on purpose
|
||||
set(SDL_UNIX_CONSOLE_BUILD ON CACHE BOOL "" FORCE)
|
||||
foreach(off SDL_SHARED SDL_TEST_LIBRARY SDL_TESTS SDL_EXAMPLES SDL_INSTALL
|
||||
SDL_X11 SDL_WAYLAND SDL_KMSDRM SDL_RPI SDL_ROCKCHIP SDL_VIVANTE SDL_OFFSCREEN SDL_DUMMYVIDEO
|
||||
SDL_OPENGL SDL_OPENGLES SDL_VULKAN SDL_GPU SDL_RENDER_GPU SDL_CAMERA SDL_OPENVR
|
||||
SDL_OSS SDL_ALSA SDL_JACK SDL_PIPEWIRE SDL_PULSEAUDIO SDL_SNDIO SDL_DISKAUDIO SDL_DUMMYAUDIO
|
||||
SDL_DBUS SDL_IBUS SDL_LIBURING SDL_LIBUDEV SDL_HIDAPI SDL_HIDAPI_LIBUSB SDL_VIRTUAL_JOYSTICK
|
||||
SDL_DLOPEN_NOTES SDL_DEPS_SHARED SDL_ASSEMBLY_UNUSED)
|
||||
set(${off} OFF CACHE BOOL "" FORCE)
|
||||
endforeach()
|
||||
add_subdirectory(${SDL_DIR} ${CMAKE_BINARY_DIR}/sdl EXCLUDE_FROM_ALL)
|
||||
set(AURORA_SDL3_PROVIDER vendor CACHE STRING "" FORCE)
|
||||
set(AURORA_SDL3_LINKAGE static CACHE STRING "" FORCE)
|
||||
|
||||
# nod
|
||||
add_library(nod_ps5 STATIC ${ps5}/nod/nod.cpp)
|
||||
target_include_directories(nod_ps5 PUBLIC ${ps5}/nod/include)
|
||||
target_compile_features(nod_ps5 PRIVATE cxx_std_20)
|
||||
add_library(nod::nod ALIAS nod_ps5)
|
||||
set(AURORA_NOD_PROVIDER vendor CACHE STRING "" FORCE)
|
||||
set(AURORA_NOD_LINKAGE static CACHE STRING "" FORCE)
|
||||
|
||||
# Tracy (compiled out, but still configured) asks a FreeBSD for libexecinfo: the
|
||||
# console has none, and nothing of it is called
|
||||
set(EXECINFO_LIBRARY ${ps5_platform_archive} CACHE STRING "" FORCE)
|
||||
|
||||
# zstd's tracing hooks are weak symbols for a tool to fill in: a title cannot
|
||||
# leave a symbol undefined (once Aurora has fetched zstd, at the end of the configuration)
|
||||
function(ps5_late_fixes)
|
||||
if(TARGET libzstd_static)
|
||||
target_compile_definitions(libzstd_static PRIVATE ZSTD_TRACE=0)
|
||||
endif()
|
||||
# Dolphin's DSP HLE includes <zlib.h> and asks for the host's library by name:
|
||||
# the headers are the zlib Aurora fetched (tools/link-title.sh drops both
|
||||
# libraries, the RADV archive has zlib whole)
|
||||
# The run harness reads frames back through Dawn's C++ header, which Dawn's
|
||||
# build generates: it has to exist before those units compile
|
||||
if(TARGET cos_pc AND TARGET webgpu_dawn)
|
||||
add_dependencies(cos_pc webgpu_dawn)
|
||||
endif()
|
||||
if(TARGET cos_dsp_hle AND TARGET ZLIB::ZLIB)
|
||||
target_link_libraries(cos_dsp_hle PRIVATE ZLIB::ZLIB)
|
||||
endif()
|
||||
endfunction()
|
||||
cmake_language(DEFER DIRECTORY ${CMAKE_SOURCE_DIR} CALL ps5_late_fixes)
|
||||
|
||||
# The game is tuned for the console's CPU, not baseline x86-64
|
||||
add_compile_options(-march=znver2)
|
||||
|
||||
# The link: every object and archive CMake would hand the linker goes to
|
||||
# PS5_Vulkan's recipe instead (RADV, the platform layer, the CRT, the fake SELF)
|
||||
set(CMAKE_CXX_LINK_EXECUTABLE
|
||||
"bash ${ps5}/tools/link-title.sh ${CMAKE_BINARY_DIR} ${PS5_VULKAN_DIR} ${PS5_PAYLOAD_SDK} <OBJECTS> @${CMAKE_BINARY_DIR}/cos_exe/objects.rsp --whole-archive ${ps5_platform_archive} --no-whole-archive <LINK_LIBRARIES>")
|
||||
# (the start-up constructor in the platform archive has no caller: hence whole.
|
||||
# objects.rsp is the game itself, in SwitchWaker's order: native/cmake/executable.cmake
|
||||
# hands it to the linker as an option, which this rule would otherwise drop)
|
||||
@@ -0,0 +1,621 @@
|
||||
#ifndef NOD_H
|
||||
#define NOD_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
/**
|
||||
* Wii disc magic bytes at offset 0x18.
|
||||
*/
|
||||
#define WII_MAGIC ((const uint8_t[4]){0x5D, 0x1C, 0x9E, 0xA3})
|
||||
|
||||
/**
|
||||
* GameCube disc magic bytes at offset 0x1C.
|
||||
*/
|
||||
#define GCN_MAGIC ((const uint8_t[4]){0xC2, 0x33, 0x9F, 0x3D})
|
||||
|
||||
/**
|
||||
* Compares two 4-byte magic values.
|
||||
*/
|
||||
#define NOD_MAGIC_EQ(lhs, rhs) ((lhs)[0] == (rhs)[0] && (lhs)[1] == (rhs)[1] && (lhs)[2] == (rhs)[2] && (lhs)[3] == (rhs)[3])
|
||||
|
||||
|
||||
/**
|
||||
* Partition kind: data partition.
|
||||
*/
|
||||
#define NOD_PARTITION_KIND_DATA 0
|
||||
|
||||
/**
|
||||
* Partition kind: update partition.
|
||||
*/
|
||||
#define NOD_PARTITION_KIND_UPDATE 1
|
||||
|
||||
/**
|
||||
* Partition kind: channel partition.
|
||||
*/
|
||||
#define NOD_PARTITION_KIND_CHANNEL 2
|
||||
|
||||
/**
|
||||
* Sentinel value: `nod_partition_find_file` returns this when a file is not found.
|
||||
* `nod_partition_iterate_fst` callback returns this to stop iteration.
|
||||
*/
|
||||
#define NOD_FST_STOP UINT32_MAX
|
||||
|
||||
/**
|
||||
* Result codes returned by nod FFI functions.
|
||||
*/
|
||||
typedef enum NodResult {
|
||||
/**
|
||||
* Operation succeeded.
|
||||
*/
|
||||
NOD_RESULT_OK,
|
||||
/**
|
||||
* An I/O error occurred.
|
||||
*/
|
||||
NOD_RESULT_ERR_IO,
|
||||
/**
|
||||
* The disc format is invalid or unsupported.
|
||||
*/
|
||||
NOD_RESULT_ERR_FORMAT,
|
||||
/**
|
||||
* The requested item was not found.
|
||||
*/
|
||||
NOD_RESULT_ERR_NOT_FOUND,
|
||||
/**
|
||||
* The provided handle is null or of the wrong type.
|
||||
*/
|
||||
NOD_RESULT_ERR_INVALID_HANDLE,
|
||||
/**
|
||||
* An unclassified error occurred.
|
||||
*/
|
||||
NOD_RESULT_ERR_OTHER,
|
||||
} NodResult;
|
||||
|
||||
/**
|
||||
* Partition encryption mode.
|
||||
*/
|
||||
typedef enum NodPartitionEncryption {
|
||||
/**
|
||||
* Partition encryption and hashes are rebuilt to match the original state,
|
||||
* if necessary. This is used for converting or verifying a disc image.
|
||||
*/
|
||||
NOD_PARTITION_ENCRYPTION_ORIGINAL,
|
||||
/**
|
||||
* Partition data will be encrypted if reading a decrypted disc image.
|
||||
* Modifies the disc header to mark partition data as encrypted.
|
||||
*/
|
||||
NOD_PARTITION_ENCRYPTION_FORCE_ENCRYPTED,
|
||||
/**
|
||||
* Partition data will be decrypted if reading an encrypted disc image.
|
||||
* Modifies the disc header to mark partition data as decrypted.
|
||||
*/
|
||||
NOD_PARTITION_ENCRYPTION_FORCE_DECRYPTED,
|
||||
/**
|
||||
* Partition data will be decrypted if reading an encrypted disc image.
|
||||
* Modifies the disc header to mark partition data as decrypted.
|
||||
* Hashes are removed from the partition data.
|
||||
*/
|
||||
NOD_PARTITION_ENCRYPTION_FORCE_DECRYPTED_NO_HASHES,
|
||||
} NodPartitionEncryption;
|
||||
|
||||
/**
|
||||
* Disc image format.
|
||||
*/
|
||||
typedef enum NodFormat {
|
||||
NOD_FORMAT_ISO,
|
||||
NOD_FORMAT_CISO,
|
||||
NOD_FORMAT_GCZ,
|
||||
NOD_FORMAT_NFS,
|
||||
NOD_FORMAT_RVZ,
|
||||
NOD_FORMAT_WBFS,
|
||||
NOD_FORMAT_WIA,
|
||||
NOD_FORMAT_TGC,
|
||||
} NodFormat;
|
||||
|
||||
/**
|
||||
* The disc file format's compression algorithm.
|
||||
*/
|
||||
typedef enum NodCompressionKind {
|
||||
NOD_COMPRESSION_KIND_NONE,
|
||||
NOD_COMPRESSION_KIND_BZIP2,
|
||||
NOD_COMPRESSION_KIND_DEFLATE,
|
||||
NOD_COMPRESSION_KIND_LZMA,
|
||||
NOD_COMPRESSION_KIND_LZMA2,
|
||||
NOD_COMPRESSION_KIND_ZSTANDARD,
|
||||
} NodCompressionKind;
|
||||
|
||||
/**
|
||||
* File system node kind.
|
||||
*/
|
||||
typedef enum NodNodeKind {
|
||||
NOD_NODE_KIND_FILE,
|
||||
NOD_NODE_KIND_DIRECTORY,
|
||||
} NodNodeKind;
|
||||
|
||||
/**
|
||||
* Opaque handle representing a disc, partition, or file reader.
|
||||
*/
|
||||
typedef struct NodHandle NodHandle;
|
||||
|
||||
/**
|
||||
* Options for opening a disc image. Zero-initialization provides sensible defaults.
|
||||
*/
|
||||
typedef struct NodDiscOptions {
|
||||
/**
|
||||
* Wii partition encryption mode. This affects how partition data appears when
|
||||
* reading directly from the disc handle, and can be used to convert between
|
||||
* encrypted and decrypted disc images.
|
||||
*/
|
||||
enum NodPartitionEncryption partition_encryption;
|
||||
/**
|
||||
* Number of threads to use for preloading data as the disc is read. This
|
||||
* is particularly useful when reading the disc image sequentially, as it
|
||||
* can perform decompression and rebuilding in parallel with the main read
|
||||
* thread. 0 disables preloading. Ignored if built without threading support.
|
||||
*/
|
||||
uint32_t preloader_threads;
|
||||
} NodDiscOptions;
|
||||
|
||||
/**
|
||||
* Reads stream data at an absolute offset.
|
||||
*
|
||||
* Returns the number of bytes read, or `-1` on error.
|
||||
*/
|
||||
typedef int64_t (*NodDiscStreamReadAtCallback)(void *user_data,
|
||||
uint64_t offset,
|
||||
void *out,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* Returns the total stream length in bytes.
|
||||
*
|
||||
* Returns the length, or `-1` on error.
|
||||
*/
|
||||
typedef int64_t (*NodDiscStreamLenCallback)(void *user_data);
|
||||
|
||||
/**
|
||||
* Closes stream resources associated with `user_data`.
|
||||
*/
|
||||
typedef void (*NodDiscStreamCloseCallback)(void *user_data);
|
||||
|
||||
/**
|
||||
* Callback-backed stream descriptor for `nod_disc_open_stream`.
|
||||
*
|
||||
* All callbacks must be non-null.
|
||||
*/
|
||||
typedef struct NodDiscStream {
|
||||
/**
|
||||
* Opaque pointer passed to callbacks.
|
||||
*/
|
||||
void *user_data;
|
||||
/**
|
||||
* Reads data from the stream at `offset` into `out`.
|
||||
*/
|
||||
NodDiscStreamReadAtCallback read_at;
|
||||
/**
|
||||
* Returns the stream length in bytes.
|
||||
*/
|
||||
NodDiscStreamLenCallback stream_len;
|
||||
/**
|
||||
* Callback for releasing stream resources.
|
||||
*/
|
||||
NodDiscStreamCloseCallback close;
|
||||
} NodDiscStream;
|
||||
|
||||
/**
|
||||
* Options for opening a partition. Zero-initialization provides sensible defaults.
|
||||
*/
|
||||
typedef struct NodPartitionOptions {
|
||||
/**
|
||||
* Wii: Validate data hashes while reading the partition, if available.
|
||||
* This significantly slows down reading.
|
||||
*/
|
||||
bool validate_hashes;
|
||||
} NodPartitionOptions;
|
||||
|
||||
/**
|
||||
* Shared GameCube & Wii disc header.
|
||||
*
|
||||
* This header is always at the start of the disc image and within each Wii partition.
|
||||
*/
|
||||
typedef struct NodDiscHeader {
|
||||
/**
|
||||
* Game ID (e.g. GM8E01 for Metroid Prime) (not null-terminated)
|
||||
*/
|
||||
char game_id[6];
|
||||
/**
|
||||
* Used in multi-disc games
|
||||
*/
|
||||
uint8_t disc_num;
|
||||
/**
|
||||
* Disc version
|
||||
*/
|
||||
uint8_t disc_version;
|
||||
/**
|
||||
* Audio streaming enabled
|
||||
*/
|
||||
uint8_t audio_streaming;
|
||||
/**
|
||||
* Audio streaming buffer size
|
||||
*/
|
||||
uint8_t audio_stream_buf_size;
|
||||
/**
|
||||
* Padding
|
||||
*/
|
||||
uint8_t _pad1[14];
|
||||
/**
|
||||
* If this is a Wii disc, this will be equal to WII_MAGIC
|
||||
*/
|
||||
uint8_t wii_magic[4];
|
||||
/**
|
||||
* If this is a GameCube disc, this will be equal to GCN_MAGIC
|
||||
*/
|
||||
uint8_t gcn_magic[4];
|
||||
/**
|
||||
* Game title (not null-terminated)
|
||||
*/
|
||||
char game_title[64];
|
||||
/**
|
||||
* If 1, disc omits partition hashes
|
||||
*/
|
||||
uint8_t no_partition_hashes;
|
||||
/**
|
||||
* If 1, disc omits partition encryption
|
||||
*/
|
||||
uint8_t no_partition_encryption;
|
||||
/**
|
||||
* Padding
|
||||
*/
|
||||
uint8_t _pad2[926];
|
||||
} NodDiscHeader;
|
||||
|
||||
/**
|
||||
* Disc compression settings.
|
||||
*/
|
||||
typedef struct NodCompression {
|
||||
/**
|
||||
* The compression algorithm.
|
||||
*/
|
||||
enum NodCompressionKind kind;
|
||||
/**
|
||||
* Compression level (0 if not applicable).
|
||||
*/
|
||||
int16_t level;
|
||||
} NodCompression;
|
||||
|
||||
/**
|
||||
* Extra metadata about the underlying disc file format.
|
||||
*/
|
||||
typedef struct NodDiscMeta {
|
||||
/**
|
||||
* The disc file format.
|
||||
*/
|
||||
enum NodFormat format;
|
||||
/**
|
||||
* The format's compression algorithm.
|
||||
*/
|
||||
struct NodCompression compression;
|
||||
/**
|
||||
* If the format uses blocks, the block size in bytes (0 if unknown).
|
||||
*/
|
||||
uint32_t block_size;
|
||||
/**
|
||||
* Whether Wii partitions are stored decrypted in the format.
|
||||
*/
|
||||
bool decrypted;
|
||||
/**
|
||||
* Whether the format omits Wii partition data hashes.
|
||||
*/
|
||||
bool needs_hash_recovery;
|
||||
/**
|
||||
* Whether the format supports recovering the original disc data losslessly.
|
||||
*/
|
||||
bool lossless;
|
||||
/**
|
||||
* The original disc's size in bytes, if stored by the format (0 if unknown).
|
||||
*/
|
||||
uint64_t disc_size;
|
||||
/**
|
||||
* The original disc's CRC32 hash, if stored by the format (0 if unknown).
|
||||
*/
|
||||
uint32_t crc32;
|
||||
/**
|
||||
* The original disc's MD5 hash, if stored by the format (all zeroes if unknown).
|
||||
*/
|
||||
uint8_t md5[16];
|
||||
/**
|
||||
* The original disc's SHA-1 hash, if stored by the format (all zeroes if unknown).
|
||||
*/
|
||||
uint8_t sha1[20];
|
||||
/**
|
||||
* The original disc's XXH64 hash, if stored by the format (0 if unknown).
|
||||
*/
|
||||
uint64_t xxh64;
|
||||
} NodDiscMeta;
|
||||
|
||||
/**
|
||||
* Partition information.
|
||||
*/
|
||||
typedef struct NodPartitionInfo {
|
||||
/**
|
||||
* Partition index.
|
||||
*/
|
||||
uint32_t index;
|
||||
/**
|
||||
* Partition kind (0 = Data, 1 = Update, 2 = Channel, other = raw value).
|
||||
*/
|
||||
uint32_t kind;
|
||||
/**
|
||||
* Data region size in bytes.
|
||||
*/
|
||||
uint64_t data_size;
|
||||
} NodPartitionInfo;
|
||||
|
||||
/**
|
||||
* Borrowed byte slice.
|
||||
*
|
||||
* The data pointer is non-owning and remains valid while the source handle is alive.
|
||||
*/
|
||||
typedef struct NodBlob {
|
||||
/**
|
||||
* Pointer to the first byte, or null if absent.
|
||||
*/
|
||||
const uint8_t *data;
|
||||
/**
|
||||
* Number of bytes in `data`.
|
||||
*/
|
||||
size_t size;
|
||||
} NodBlob;
|
||||
|
||||
/**
|
||||
* Extra partition metadata blobs. (boot.bin, bi2.bin, apploader.bin, main.dol, etc.)
|
||||
*
|
||||
* All pointers are borrowed and valid while the source partition handle remains alive.
|
||||
*/
|
||||
typedef struct NodPartitionMeta {
|
||||
/**
|
||||
* Disc and boot header (boot.bin)
|
||||
*/
|
||||
struct NodBlob raw_boot;
|
||||
/**
|
||||
* Debug and region information (bi2.bin)
|
||||
*/
|
||||
struct NodBlob raw_bi2;
|
||||
/**
|
||||
* Apploader (apploader.bin)
|
||||
*/
|
||||
struct NodBlob raw_apploader;
|
||||
/**
|
||||
* Main binary (main.dol)
|
||||
*/
|
||||
struct NodBlob raw_dol;
|
||||
/**
|
||||
* File system table (fst.bin)
|
||||
*/
|
||||
struct NodBlob raw_fst;
|
||||
/**
|
||||
* Ticket (ticket.bin, Wii only)
|
||||
*/
|
||||
struct NodBlob raw_ticket;
|
||||
/**
|
||||
* TMD (tmd.bin, Wii only)
|
||||
*/
|
||||
struct NodBlob raw_tmd;
|
||||
/**
|
||||
* Certificate chain (cert.bin, Wii only)
|
||||
*/
|
||||
struct NodBlob raw_cert_chain;
|
||||
/**
|
||||
* H3 hash table (h3.bin, Wii only)
|
||||
*/
|
||||
struct NodBlob raw_h3_table;
|
||||
} NodPartitionMeta;
|
||||
|
||||
/**
|
||||
* Callback for iterating file system entries.
|
||||
*
|
||||
* Parameters:
|
||||
* - `index`: node index in the FST
|
||||
* - `kind`: whether the node is a file or directory
|
||||
* - `name`: null-terminated file/directory name
|
||||
* - `size`: file size in bytes (0 for directories)
|
||||
* - `user_data`: opaque pointer passed to `nod_partition_iterate_fst`
|
||||
*
|
||||
* Returns: the next node index to visit, or `NOD_FST_STOP` to stop.
|
||||
*/
|
||||
typedef uint32_t (*NodFstCallback)(uint32_t, enum NodNodeKind, const char*, uint32_t, void*);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif // __cplusplus
|
||||
|
||||
/**
|
||||
* Returns a pointer to the last error message, or null if no error has occurred.
|
||||
*
|
||||
* The returned string is valid until the next `nod_*` call on the same thread.
|
||||
*/
|
||||
const char *nod_error_message(void);
|
||||
|
||||
/**
|
||||
* Opens a disc image from a file path.
|
||||
*
|
||||
* `options` may be null to use defaults.
|
||||
*
|
||||
* On success, writes a new handle to `*out` and returns `NOD_RESULT_OK`.
|
||||
* The handle must be freed with `nod_free`.
|
||||
*/
|
||||
enum NodResult nod_disc_open(const char *path,
|
||||
const struct NodDiscOptions *options,
|
||||
struct NodHandle **out);
|
||||
|
||||
/**
|
||||
* Opens a disc image from a callback-backed stream.
|
||||
*
|
||||
* `stream` callbacks must be valid and non-null.
|
||||
* `options` may be null to use defaults.
|
||||
*
|
||||
* On success, writes a new handle to `*out` and returns `NOD_RESULT_OK`.
|
||||
* The handle must be freed with `nod_free`.
|
||||
*/
|
||||
enum NodResult nod_disc_open_stream(const struct NodDiscStream *stream,
|
||||
const struct NodDiscOptions *options,
|
||||
struct NodHandle **out);
|
||||
|
||||
/**
|
||||
* Opens a partition by index from a disc handle.
|
||||
*
|
||||
* `disc` must be a handle returned by `nod_disc_open`.
|
||||
* `options` may be null to use defaults.
|
||||
*
|
||||
* **GameCube**: `index` must always be 0.
|
||||
*
|
||||
* On success, writes a new handle to `*out` and returns `NOD_RESULT_OK`.
|
||||
* The partition handle must be freed with `nod_free`.
|
||||
*/
|
||||
enum NodResult nod_disc_open_partition(struct NodHandle *disc,
|
||||
uint32_t index,
|
||||
const struct NodPartitionOptions *options,
|
||||
struct NodHandle **out);
|
||||
|
||||
/**
|
||||
* Opens a partition by kind from a disc handle.
|
||||
*
|
||||
* Searches for the first partition matching `kind` (0 = Data, 1 = Update, 2 = Channel).
|
||||
* `options` may be null to use defaults.
|
||||
*
|
||||
* **GameCube**: `kind` must always be `NOD_PARTITION_KIND_DATA`.
|
||||
*
|
||||
* On success, writes a new handle to `*out` and returns `NOD_RESULT_OK`.
|
||||
* The partition handle must be freed with `nod_free`.
|
||||
*/
|
||||
enum NodResult nod_disc_open_partition_kind(struct NodHandle *disc,
|
||||
uint32_t kind,
|
||||
const struct NodPartitionOptions *options,
|
||||
struct NodHandle **out);
|
||||
|
||||
/**
|
||||
* Opens a file from a partition handle by FST node index.
|
||||
*
|
||||
* `partition` must be a handle returned by `nod_disc_open_partition` or
|
||||
* `nod_disc_open_partition_kind`.
|
||||
* On success, writes a new handle to `*out` and returns `NOD_RESULT_OK`.
|
||||
* The file handle must be freed with `nod_free`.
|
||||
*/
|
||||
enum NodResult nod_partition_open_file(struct NodHandle *partition,
|
||||
uint32_t fst_index,
|
||||
struct NodHandle **out);
|
||||
|
||||
/**
|
||||
* Frees a handle returned by any `nod_*_open*` function.
|
||||
*
|
||||
* Passing null is a no-op. After this call, the handle is invalid.
|
||||
*/
|
||||
void nod_free(struct NodHandle *handle);
|
||||
|
||||
/**
|
||||
* Reads up to `len` bytes from a handle into `buf`.
|
||||
*
|
||||
* This function may return fewer than `len` bytes even before end-of-stream.
|
||||
* Callers that need an exact byte count must loop until the buffer is filled
|
||||
* or the function returns 0 (end-of-stream) / -1 (error).
|
||||
*
|
||||
* Returns the number of bytes read, or -1 on error.
|
||||
* Returns 0 at end-of-stream.
|
||||
*/
|
||||
int64_t nod_read(struct NodHandle *handle, uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* Seeks a handle to a new position.
|
||||
*
|
||||
* `whence`: 0 = from start, 1 = from current, 2 = from end.
|
||||
* Returns the new absolute position, or -1 on error.
|
||||
*/
|
||||
int64_t nod_seek(struct NodHandle *handle, int64_t offset, int32_t whence);
|
||||
|
||||
/**
|
||||
* Returns a pointer to the internal buffer and its length for zero-copy reads.
|
||||
*
|
||||
* On success, `*out_len` is set to the number of available bytes and a pointer
|
||||
* to the buffer is returned. Returns null on error or end-of-stream.
|
||||
* Call `nod_buf_consume` after processing the data.
|
||||
*/
|
||||
const void *nod_buf_read(struct NodHandle *handle, size_t *out_len);
|
||||
|
||||
/**
|
||||
* Consumes `n` bytes from the internal buffer after a `nod_buf_read` call.
|
||||
*/
|
||||
void nod_buf_consume(struct NodHandle *handle, size_t n);
|
||||
|
||||
/**
|
||||
* Copies the disc header into the provided struct.
|
||||
*
|
||||
* `disc` must be a disc handle. `out` must point to a valid `NodDiscHeader`.
|
||||
*/
|
||||
enum NodResult nod_disc_header(const struct NodHandle *disc, struct NodDiscHeader *out);
|
||||
|
||||
/**
|
||||
* Copies the disc metadata into the provided struct.
|
||||
*
|
||||
* `disc` must be a disc handle. `out` must point to a valid `NodDiscMeta`.
|
||||
*/
|
||||
enum NodResult nod_disc_meta(const struct NodHandle *disc, struct NodDiscMeta *out);
|
||||
|
||||
/**
|
||||
* Returns the disc size in bytes.
|
||||
*
|
||||
* `disc` must be a disc handle. Returns 0 if the handle is invalid.
|
||||
*/
|
||||
uint64_t nod_disc_size(const struct NodHandle *disc);
|
||||
|
||||
/**
|
||||
* Copies partition info into a caller-provided array.
|
||||
*
|
||||
* `out` is a pointer to an array of `NodPartitionInfo` with capacity `cap`.
|
||||
* Returns the total number of partitions (which may exceed `cap`).
|
||||
*/
|
||||
size_t nod_disc_partitions(const struct NodHandle *disc, struct NodPartitionInfo *out, size_t cap);
|
||||
|
||||
/**
|
||||
* Returns whether the partition is from a Wii disc.
|
||||
*
|
||||
* `partition` must be a partition handle. Returns false if the handle is invalid.
|
||||
*/
|
||||
bool nod_partition_is_wii(const struct NodHandle *partition);
|
||||
|
||||
/**
|
||||
* Copies partition metadata blob pointers and sizes into the provided struct.
|
||||
*
|
||||
* `partition` must be a partition handle. `out` must point to a valid
|
||||
* `NodPartitionMeta`. Returned pointers are borrowed and remain valid while
|
||||
* the partition handle is alive.
|
||||
*/
|
||||
enum NodResult nod_partition_meta(const struct NodHandle *partition, struct NodPartitionMeta *out);
|
||||
|
||||
/**
|
||||
* Finds a file in the partition's file system table by path.
|
||||
*
|
||||
* If found, `*out_kind` and `*out_length` are set and the FST node index is returned.
|
||||
* Returns `NOD_FST_STOP` (`UINT32_MAX`) if the file is not found.
|
||||
*/
|
||||
uint32_t nod_partition_find_file(const struct NodHandle *partition,
|
||||
const char *path,
|
||||
enum NodNodeKind *out_kind,
|
||||
uint32_t *out_length);
|
||||
|
||||
/**
|
||||
* Iterates over all entries in the partition's file system table.
|
||||
*
|
||||
* The callback receives each node's index, kind, name, size, and user data.
|
||||
* Return the next index to visit from the callback, or `NOD_FST_STOP` to stop.
|
||||
* For normal sequential iteration, return `index + 1`.
|
||||
* For directories, return `size` (which is the child-end index) to skip the subtree.
|
||||
*/
|
||||
void nod_partition_iterate_fst(const struct NodHandle *partition,
|
||||
NodFstCallback callback,
|
||||
void *user_data);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif /* NOD_H */
|
||||
@@ -0,0 +1,599 @@
|
||||
/*
|
||||
* Wind Waker for the PlayStation 5 - the part of nod's C API Aurora and Borealis
|
||||
* call, for a GameCube disc image as .iso or .ciso.
|
||||
*
|
||||
* nod (https://github.com/encounter/nod, include/nod.h is its header at the
|
||||
* release Aurora pins) is a Rust library that reads every GameCube and Wii
|
||||
* image format. There is no Rust toolchain for the console, and a GameCube
|
||||
* .iso needs none of it: the disc is unencrypted, with a boot header, an
|
||||
* apploader, a DOL and a file system table at fixed places. This implements
|
||||
* the functions the title links, with nod's behaviour, for that one format.
|
||||
* CISO, which only leaves out the disc's empty blocks, is read as well. Compressed
|
||||
* images (RVZ, GCZ, WIA, WBFS) and Wii discs are refused with a message.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include "nod.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
thread_local std::string t_error;
|
||||
|
||||
NodResult fail(NodResult result, const char* format, ...) {
|
||||
char message[256];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(message, sizeof(message), format, args);
|
||||
va_end(args);
|
||||
t_error = message;
|
||||
return result;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
constexpr size_t kBootSize = 0x440;
|
||||
constexpr size_t kBi2Size = 0x2000;
|
||||
constexpr uint64_t kApploaderOffset = kBootSize + kBi2Size;
|
||||
constexpr size_t kMaxSystemBlob = 64u << 20;
|
||||
|
||||
constexpr uint64_t kGameCubeDiscSize = 1459978240;
|
||||
constexpr size_t kCisoHeaderSize = 0x8000;
|
||||
constexpr size_t kCisoMapSize = kCisoHeaderSize - 8;
|
||||
|
||||
// The image, shared by every handle opened from it
|
||||
struct Disc {
|
||||
NodDiscStream stream{};
|
||||
uint64_t fileSize = 0; // the stream's
|
||||
uint64_t size = 0; // the disc's
|
||||
std::mutex lock; // the stream's callbacks are not promised to be reentrant
|
||||
// CISO: the disc in blocks, each either stored (in order, after the header) or all zero
|
||||
uint32_t cisoBlockSize = 0;
|
||||
std::vector<int32_t> cisoBlocks; // a block's place among the stored ones, or -1
|
||||
|
||||
~Disc() {
|
||||
if (stream.close != nullptr) {
|
||||
stream.close(stream.user_data);
|
||||
}
|
||||
}
|
||||
|
||||
// The stream itself; the lock is held
|
||||
int64_t readFile(uint64_t offset, uint8_t* out, size_t len) {
|
||||
size_t done = 0;
|
||||
while (done < len) {
|
||||
const int64_t n = stream.read_at(stream.user_data, offset + done, out + done, len - done);
|
||||
if (n < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) {
|
||||
break;
|
||||
}
|
||||
done += static_cast<size_t>(n);
|
||||
}
|
||||
return static_cast<int64_t>(done);
|
||||
}
|
||||
|
||||
// The disc
|
||||
int64_t readAt(uint64_t offset, void* out, size_t len) {
|
||||
if (offset >= size) {
|
||||
return 0;
|
||||
}
|
||||
len = static_cast<size_t>(std::min<uint64_t>(len, size - offset));
|
||||
auto* p = static_cast<uint8_t*>(out);
|
||||
std::lock_guard guard{lock};
|
||||
if (cisoBlockSize == 0) {
|
||||
return readFile(offset, p, len);
|
||||
}
|
||||
size_t done = 0;
|
||||
while (done < len) {
|
||||
const uint64_t at = offset + done;
|
||||
const uint64_t block = at / cisoBlockSize;
|
||||
const uint64_t within = at % cisoBlockSize;
|
||||
const size_t part = static_cast<size_t>(std::min<uint64_t>(len - done, cisoBlockSize - within));
|
||||
const int32_t stored = block < cisoBlocks.size() ? cisoBlocks[block] : -1;
|
||||
if (stored < 0) {
|
||||
memset(p + done, 0, part);
|
||||
} else {
|
||||
const int64_t n =
|
||||
readFile(kCisoHeaderSize + uint64_t(stored) * cisoBlockSize + within, p + done, part);
|
||||
if (n < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (static_cast<size_t>(n) < part) {
|
||||
memset(p + done + n, 0, part - static_cast<size_t>(n)); // a stored block cut short
|
||||
}
|
||||
}
|
||||
done += part;
|
||||
}
|
||||
return static_cast<int64_t>(done);
|
||||
}
|
||||
|
||||
bool readExact(uint64_t offset, void* out, size_t len) {
|
||||
return readAt(offset, out, len) == static_cast<int64_t>(len);
|
||||
}
|
||||
};
|
||||
|
||||
struct Partition {
|
||||
std::vector<uint8_t> boot, bi2, apploader, dol, fst;
|
||||
uint32_t entries = 0; // FST entries, the root included
|
||||
const char* names = nullptr; // the FST's string table
|
||||
size_t namesSize = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct NodHandle {
|
||||
enum class Kind { Disc, Partition, File } kind = Kind::Disc;
|
||||
std::shared_ptr<Disc> disc;
|
||||
std::shared_ptr<Partition> partition;
|
||||
// Disc and File: a window of the image, and the position in it
|
||||
uint64_t start = 0;
|
||||
uint64_t length = 0;
|
||||
uint64_t position = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
bool readBlob(Disc& disc, uint64_t offset, uint64_t size, std::vector<uint8_t>& out, const char* what) {
|
||||
if (size > kMaxSystemBlob || offset + size > disc.size) {
|
||||
fail(NOD_RESULT_ERR_FORMAT, "%s is outside the disc image (offset 0x%llx, size 0x%llx)", what,
|
||||
static_cast<unsigned long long>(offset), static_cast<unsigned long long>(size));
|
||||
return false;
|
||||
}
|
||||
out.resize(static_cast<size_t>(size));
|
||||
if (!disc.readExact(offset, out.data(), out.size())) {
|
||||
fail(NOD_RESULT_ERR_IO, "failed to read %s", what);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<Partition> loadPartition(Disc& disc) {
|
||||
auto partition = std::make_shared<Partition>();
|
||||
if (!readBlob(disc, 0, kBootSize, partition->boot, "the boot header") ||
|
||||
!readBlob(disc, kBootSize, kBi2Size, partition->bi2, "bi2")) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint8_t* boot = partition->boot.data();
|
||||
const uint32_t dolOffset = be32(boot + 0x420);
|
||||
const uint32_t fstOffset = be32(boot + 0x424);
|
||||
const uint32_t fstSize = be32(boot + 0x428);
|
||||
|
||||
// The apploader: a 0x20 byte header, then its code and its trailer
|
||||
uint8_t apploaderHeader[0x20];
|
||||
if (!disc.readExact(kApploaderOffset, apploaderHeader, sizeof(apploaderHeader))) {
|
||||
fail(NOD_RESULT_ERR_IO, "failed to read the apploader header");
|
||||
return nullptr;
|
||||
}
|
||||
const uint64_t apploaderSize =
|
||||
0x20 + uint64_t(be32(apploaderHeader + 0x14)) + uint64_t(be32(apploaderHeader + 0x18));
|
||||
if (!readBlob(disc, kApploaderOffset, apploaderSize, partition->apploader, "the apploader")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The DOL: 7 text and 11 data sections, an offset and a size each
|
||||
uint8_t dolHeader[0x100];
|
||||
if (!disc.readExact(dolOffset, dolHeader, sizeof(dolHeader))) {
|
||||
fail(NOD_RESULT_ERR_IO, "failed to read the DOL header");
|
||||
return nullptr;
|
||||
}
|
||||
uint64_t dolSize = sizeof(dolHeader);
|
||||
for (int section = 0; section < 18; section++) {
|
||||
const uint64_t offset = be32(dolHeader + section * 4);
|
||||
const uint64_t size = be32(dolHeader + 0x90 + section * 4);
|
||||
if (size != 0) {
|
||||
dolSize = std::max(dolSize, offset + size);
|
||||
}
|
||||
}
|
||||
if (!readBlob(disc, dolOffset, dolSize, partition->dol, "the DOL") ||
|
||||
!readBlob(disc, fstOffset, fstSize, partition->fst, "the file system table")) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (partition->fst.size() < 12) {
|
||||
fail(NOD_RESULT_ERR_FORMAT, "the file system table is empty");
|
||||
return nullptr;
|
||||
}
|
||||
partition->entries = be32(partition->fst.data() + 8);
|
||||
if (partition->entries == 0 || uint64_t(partition->entries) * 12 > partition->fst.size()) {
|
||||
fail(NOD_RESULT_ERR_FORMAT, "the file system table has %u entries in %zu bytes", partition->entries,
|
||||
partition->fst.size());
|
||||
return nullptr;
|
||||
}
|
||||
partition->names = reinterpret_cast<const char*>(partition->fst.data()) + size_t(partition->entries) * 12;
|
||||
partition->namesSize = partition->fst.size() - size_t(partition->entries) * 12;
|
||||
// every name must end inside the table
|
||||
if (partition->namesSize == 0 || partition->names[partition->namesSize - 1] != '\0') {
|
||||
partition->fst.push_back(0);
|
||||
partition->names = reinterpret_cast<const char*>(partition->fst.data()) + size_t(partition->entries) * 12;
|
||||
partition->namesSize++;
|
||||
}
|
||||
return partition;
|
||||
}
|
||||
|
||||
struct FstEntry {
|
||||
bool directory;
|
||||
const char* name;
|
||||
uint32_t offset; // a file's place in the image; a directory's parent
|
||||
uint32_t size; // a file's length; a directory's end index
|
||||
};
|
||||
|
||||
bool fstEntry(const Partition& partition, uint32_t index, FstEntry& out) {
|
||||
if (index >= partition.entries) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t* raw = partition.fst.data() + size_t(index) * 12;
|
||||
const uint32_t nameOffset = be32(raw) & 0x00ffffff;
|
||||
out.directory = raw[0] != 0;
|
||||
out.name = nameOffset < partition.namesSize ? partition.names + nameOffset : "";
|
||||
out.offset = be32(raw + 4);
|
||||
out.size = be32(raw + 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sameName(const char* a, const char* b, size_t bLength) {
|
||||
size_t i = 0;
|
||||
for (; i < bLength; i++) {
|
||||
const auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? char(c + 32) : c; };
|
||||
if (a[i] == '\0' || lower(a[i]) != lower(b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a[i] == '\0';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
const char* nod_error_message(void) { return t_error.empty() ? nullptr : t_error.c_str(); }
|
||||
|
||||
NodResult nod_disc_open_stream(const NodDiscStream* stream, const NodDiscOptions* options, NodHandle** out) {
|
||||
(void)options;
|
||||
if (out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_OTHER, "no output handle");
|
||||
}
|
||||
*out = nullptr;
|
||||
if (stream == nullptr || stream->read_at == nullptr || stream->stream_len == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_OTHER, "the disc stream has no read or length callback");
|
||||
}
|
||||
auto disc = std::make_shared<Disc>();
|
||||
disc->stream = *stream; // owned from here: closed with the last handle
|
||||
const int64_t length = stream->stream_len(stream->user_data);
|
||||
if (length < 0) {
|
||||
return fail(NOD_RESULT_ERR_IO, "failed to get the disc image's size");
|
||||
}
|
||||
disc->fileSize = disc->size = static_cast<uint64_t>(length);
|
||||
|
||||
// CISO: "CISO", the block size, then a byte a block that says whether it is stored
|
||||
{
|
||||
std::vector<uint8_t> ciso(kCisoHeaderSize);
|
||||
if (disc->fileSize >= kCisoHeaderSize && disc->readExact(0, ciso.data(), 8) && memcmp(ciso.data(), "CISO", 4) == 0) {
|
||||
if (!disc->readExact(0, ciso.data(), ciso.size())) {
|
||||
return fail(NOD_RESULT_ERR_IO, "failed to read the CISO header");
|
||||
}
|
||||
const uint32_t blockSize = uint32_t(ciso[4]) | (uint32_t(ciso[5]) << 8) | (uint32_t(ciso[6]) << 16) |
|
||||
(uint32_t(ciso[7]) << 24);
|
||||
if (blockSize < 0x8000 || (blockSize & (blockSize - 1)) != 0) {
|
||||
return fail(NOD_RESULT_ERR_FORMAT, "the CISO image has a block size of %u", blockSize);
|
||||
}
|
||||
std::vector<int32_t> blocks(kCisoMapSize, -1);
|
||||
int32_t stored = 0;
|
||||
size_t used = 0;
|
||||
for (size_t block = 0; block < kCisoMapSize; block++) {
|
||||
if (ciso[8 + block] != 0) {
|
||||
blocks[block] = stored++;
|
||||
used = block + 1;
|
||||
}
|
||||
}
|
||||
if (used == 0) {
|
||||
return fail(NOD_RESULT_ERR_FORMAT, "the CISO image stores no block");
|
||||
}
|
||||
blocks.resize(used);
|
||||
disc->cisoBlocks = std::move(blocks);
|
||||
disc->cisoBlockSize = blockSize;
|
||||
// A GameCube disc's size is not a whole number of blocks: the last one is cut to it
|
||||
disc->size = uint64_t(used) * blockSize;
|
||||
if (disc->size > kGameCubeDiscSize && disc->size - kGameCubeDiscSize < blockSize) {
|
||||
disc->size = kGameCubeDiscSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t header[0x20];
|
||||
if (!disc->readExact(0, header, sizeof(header))) {
|
||||
return fail(NOD_RESULT_ERR_FORMAT, "the file is too small to be a disc image");
|
||||
}
|
||||
if (!NOD_MAGIC_EQ(header + 0x1c, GCN_MAGIC)) {
|
||||
if (NOD_MAGIC_EQ(header + 0x18, WII_MAGIC)) {
|
||||
return fail(NOD_RESULT_ERR_FORMAT,
|
||||
"Wii disc images are not supported on PlayStation 5: use the GameCube version");
|
||||
}
|
||||
return fail(NOD_RESULT_ERR_FORMAT,
|
||||
"not a GameCube .iso or .ciso disc image: convert the dump first (RVZ, GCZ, WIA and WBFS "
|
||||
"are not supported on PlayStation 5)");
|
||||
}
|
||||
|
||||
auto* handle = new NodHandle;
|
||||
handle->kind = NodHandle::Kind::Disc;
|
||||
handle->length = disc->size;
|
||||
handle->disc = std::move(disc);
|
||||
*out = handle;
|
||||
return NOD_RESULT_OK;
|
||||
}
|
||||
|
||||
NodResult nod_disc_open(const char* path, const NodDiscOptions* options, NodHandle** out) {
|
||||
(void)options;
|
||||
if (out != nullptr) {
|
||||
*out = nullptr;
|
||||
}
|
||||
if (path == nullptr || out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_OTHER, "no path or output handle");
|
||||
}
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (file == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_IO, "failed to open %s", path);
|
||||
}
|
||||
const NodDiscStream stream{
|
||||
.user_data = file,
|
||||
.read_at = [](void* user, uint64_t offset, void* buffer, size_t len) -> int64_t {
|
||||
auto* f = static_cast<FILE*>(user);
|
||||
if (fseeko(f, static_cast<off_t>(offset), SEEK_SET) != 0) {
|
||||
return -1;
|
||||
}
|
||||
const size_t n = fread(buffer, 1, len, f);
|
||||
return n == 0 && ferror(f) ? -1 : static_cast<int64_t>(n);
|
||||
},
|
||||
.stream_len = [](void* user) -> int64_t {
|
||||
auto* f = static_cast<FILE*>(user);
|
||||
if (fseeko(f, 0, SEEK_END) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return static_cast<int64_t>(ftello(f));
|
||||
},
|
||||
.close = [](void* user) { fclose(static_cast<FILE*>(user)); },
|
||||
};
|
||||
return nod_disc_open_stream(&stream, options, out);
|
||||
}
|
||||
|
||||
NodResult nod_disc_open_partition_kind(NodHandle* disc, uint32_t kind, const NodPartitionOptions* options,
|
||||
NodHandle** out) {
|
||||
(void)options;
|
||||
if (out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_OTHER, "no output handle");
|
||||
}
|
||||
*out = nullptr;
|
||||
if (disc == nullptr || disc->kind != NodHandle::Kind::Disc) {
|
||||
return fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a disc handle");
|
||||
}
|
||||
if (kind != NOD_PARTITION_KIND_DATA) {
|
||||
return fail(NOD_RESULT_ERR_NOT_FOUND, "a GameCube disc has a data partition only");
|
||||
}
|
||||
auto partition = loadPartition(*disc->disc);
|
||||
if (!partition) {
|
||||
return NOD_RESULT_ERR_FORMAT;
|
||||
}
|
||||
auto* handle = new NodHandle;
|
||||
handle->kind = NodHandle::Kind::Partition;
|
||||
handle->disc = disc->disc;
|
||||
handle->partition = std::move(partition);
|
||||
*out = handle;
|
||||
return NOD_RESULT_OK;
|
||||
}
|
||||
|
||||
NodResult nod_disc_open_partition(NodHandle* disc, uint32_t index, const NodPartitionOptions* options,
|
||||
NodHandle** out) {
|
||||
if (index != 0) {
|
||||
if (out != nullptr) {
|
||||
*out = nullptr;
|
||||
}
|
||||
return fail(NOD_RESULT_ERR_NOT_FOUND, "a GameCube disc has one partition");
|
||||
}
|
||||
return nod_disc_open_partition_kind(disc, NOD_PARTITION_KIND_DATA, options, out);
|
||||
}
|
||||
|
||||
NodResult nod_partition_open_file(NodHandle* partition, uint32_t fst_index, NodHandle** out) {
|
||||
if (out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_OTHER, "no output handle");
|
||||
}
|
||||
*out = nullptr;
|
||||
if (partition == nullptr || partition->kind != NodHandle::Kind::Partition) {
|
||||
return fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a partition handle");
|
||||
}
|
||||
FstEntry entry;
|
||||
if (!fstEntry(*partition->partition, fst_index, entry) || entry.directory) {
|
||||
return fail(NOD_RESULT_ERR_NOT_FOUND, "no file at index %u of the file system table", fst_index);
|
||||
}
|
||||
auto* handle = new NodHandle;
|
||||
handle->kind = NodHandle::Kind::File;
|
||||
handle->disc = partition->disc;
|
||||
handle->start = entry.offset;
|
||||
handle->length = entry.size;
|
||||
*out = handle;
|
||||
return NOD_RESULT_OK;
|
||||
}
|
||||
|
||||
void nod_free(NodHandle* handle) { delete handle; }
|
||||
|
||||
int64_t nod_read(NodHandle* handle, uint8_t* buf, size_t len) {
|
||||
if (handle == nullptr || handle->kind == NodHandle::Kind::Partition) {
|
||||
fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a readable handle");
|
||||
return -1;
|
||||
}
|
||||
if (handle->position >= handle->length) {
|
||||
return 0;
|
||||
}
|
||||
len = static_cast<size_t>(std::min<uint64_t>(len, handle->length - handle->position));
|
||||
const int64_t n = handle->disc->readAt(handle->start + handle->position, buf, len);
|
||||
if (n < 0) {
|
||||
fail(NOD_RESULT_ERR_IO, "failed to read the disc image");
|
||||
return -1;
|
||||
}
|
||||
handle->position += static_cast<uint64_t>(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
int64_t nod_seek(NodHandle* handle, int64_t offset, int32_t whence) {
|
||||
if (handle == nullptr || handle->kind == NodHandle::Kind::Partition) {
|
||||
fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a seekable handle");
|
||||
return -1;
|
||||
}
|
||||
int64_t base;
|
||||
switch (whence) {
|
||||
case 0:
|
||||
base = 0;
|
||||
break;
|
||||
case 1:
|
||||
base = static_cast<int64_t>(handle->position);
|
||||
break;
|
||||
case 2:
|
||||
base = static_cast<int64_t>(handle->length);
|
||||
break;
|
||||
default:
|
||||
fail(NOD_RESULT_ERR_OTHER, "seek from an unknown origin %d", whence);
|
||||
return -1;
|
||||
}
|
||||
if (base + offset < 0) {
|
||||
fail(NOD_RESULT_ERR_OTHER, "seek before the start");
|
||||
return -1;
|
||||
}
|
||||
handle->position = static_cast<uint64_t>(base + offset);
|
||||
return static_cast<int64_t>(handle->position);
|
||||
}
|
||||
|
||||
NodResult nod_disc_header(const NodHandle* disc, NodDiscHeader* out) {
|
||||
if (disc == nullptr || disc->kind != NodHandle::Kind::Disc || out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a disc handle");
|
||||
}
|
||||
static_assert(sizeof(NodDiscHeader) == 0x400);
|
||||
if (!disc->disc->readExact(0, out, sizeof(*out))) {
|
||||
return fail(NOD_RESULT_ERR_IO, "failed to read the disc header");
|
||||
}
|
||||
return NOD_RESULT_OK;
|
||||
}
|
||||
|
||||
NodResult nod_disc_meta(const NodHandle* disc, NodDiscMeta* out) {
|
||||
if (disc == nullptr || disc->kind != NodHandle::Kind::Disc || out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a disc handle");
|
||||
}
|
||||
*out = NodDiscMeta{};
|
||||
out->format = disc->disc->cisoBlockSize != 0 ? NOD_FORMAT_CISO : NOD_FORMAT_ISO;
|
||||
out->lossless = true;
|
||||
out->disc_size = disc->disc->size;
|
||||
return NOD_RESULT_OK;
|
||||
}
|
||||
|
||||
uint64_t nod_disc_size(const NodHandle* disc) {
|
||||
return disc != nullptr && disc->kind == NodHandle::Kind::Disc ? disc->disc->size : 0;
|
||||
}
|
||||
|
||||
size_t nod_disc_partitions(const NodHandle* disc, NodPartitionInfo* out, size_t cap) {
|
||||
if (disc == nullptr || disc->kind != NodHandle::Kind::Disc) {
|
||||
return 0;
|
||||
}
|
||||
if (out != nullptr && cap > 0) {
|
||||
out[0] = NodPartitionInfo{.index = 0, .kind = NOD_PARTITION_KIND_DATA, .data_size = disc->disc->size};
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool nod_partition_is_wii(const NodHandle* partition) {
|
||||
(void)partition;
|
||||
return false;
|
||||
}
|
||||
|
||||
NodResult nod_partition_meta(const NodHandle* partition, NodPartitionMeta* out) {
|
||||
if (partition == nullptr || partition->kind != NodHandle::Kind::Partition || out == nullptr) {
|
||||
return fail(NOD_RESULT_ERR_INVALID_HANDLE, "not a partition handle");
|
||||
}
|
||||
const Partition& p = *partition->partition;
|
||||
*out = NodPartitionMeta{};
|
||||
out->raw_boot = {p.boot.data(), p.boot.size()};
|
||||
out->raw_bi2 = {p.bi2.data(), p.bi2.size()};
|
||||
out->raw_apploader = {p.apploader.data(), p.apploader.size()};
|
||||
out->raw_dol = {p.dol.data(), p.dol.size()};
|
||||
out->raw_fst = {p.fst.data(), size_t(p.entries) * 12 + p.namesSize};
|
||||
return NOD_RESULT_OK;
|
||||
}
|
||||
|
||||
uint32_t nod_partition_find_file(const NodHandle* partition, const char* path, NodNodeKind* out_kind,
|
||||
uint32_t* out_length) {
|
||||
if (partition == nullptr || partition->kind != NodHandle::Kind::Partition || path == nullptr) {
|
||||
return NOD_FST_STOP;
|
||||
}
|
||||
const Partition& p = *partition->partition;
|
||||
uint32_t index = 0; // the directory searched: the root
|
||||
uint32_t end = p.entries; // its end
|
||||
FstEntry entry{.directory = true, .name = "", .offset = 0, .size = p.entries};
|
||||
while (*path != '\0') {
|
||||
while (*path == '/') {
|
||||
path++;
|
||||
}
|
||||
const char* slash = strchr(path, '/');
|
||||
const size_t length = slash != nullptr ? size_t(slash - path) : strlen(path);
|
||||
if (length == 0) {
|
||||
break;
|
||||
}
|
||||
if (!entry.directory) {
|
||||
return NOD_FST_STOP; // a file is not a folder
|
||||
}
|
||||
bool found = false;
|
||||
for (uint32_t child = index + 1; child < end && child < p.entries;) {
|
||||
FstEntry candidate;
|
||||
if (!fstEntry(p, child, candidate)) {
|
||||
break;
|
||||
}
|
||||
if (sameName(candidate.name, path, length)) {
|
||||
index = child;
|
||||
entry = candidate;
|
||||
end = candidate.directory ? candidate.size : child + 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
child = candidate.directory ? std::max(candidate.size, child + 1) : child + 1;
|
||||
}
|
||||
if (!found) {
|
||||
return NOD_FST_STOP;
|
||||
}
|
||||
path += length;
|
||||
}
|
||||
if (out_kind != nullptr) {
|
||||
*out_kind = entry.directory ? NOD_NODE_KIND_DIRECTORY : NOD_NODE_KIND_FILE;
|
||||
}
|
||||
if (out_length != nullptr) {
|
||||
*out_length = entry.directory ? 0 : entry.size;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void nod_partition_iterate_fst(const NodHandle* partition, NodFstCallback callback, void* user_data) {
|
||||
if (partition == nullptr || partition->kind != NodHandle::Kind::Partition || callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
const Partition& p = *partition->partition;
|
||||
// The root, entry 0, is not reported
|
||||
for (uint32_t index = 1; index < p.entries;) {
|
||||
FstEntry entry;
|
||||
if (!fstEntry(p, index, entry)) {
|
||||
break;
|
||||
}
|
||||
const uint32_t next = callback(index, entry.directory ? NOD_NODE_KIND_DIRECTORY : NOD_NODE_KIND_FILE,
|
||||
entry.name, entry.size, user_data);
|
||||
if (next == NOD_FST_STOP || next <= index) {
|
||||
break;
|
||||
}
|
||||
index = next;
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,18 @@
|
||||
diff --git a/lib/dawn/BackendBinding.cpp b/lib/dawn/BackendBinding.cpp
|
||||
index 447ead8..8ce4942 100644
|
||||
--- a/lib/dawn/BackendBinding.cpp
|
||||
+++ b/lib/dawn/BackendBinding.cpp
|
||||
@@ -28,6 +28,13 @@ std::shared_ptr<wgpu::ChainedStruct> SetupWindowAndGetSurfaceDescriptor(SDL_Wind
|
||||
desc->hwnd = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
|
||||
desc->hinstance = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_INSTANCE_POINTER, nullptr);
|
||||
return std::move(desc);
|
||||
+#elif defined(__PROSPERO__)
|
||||
+ // PlayStation 5: one output and no window. Dawn's PS5 port takes the Switch's
|
||||
+ // native-window descriptor for it (VK_KHR_display underneath); any token will do.
|
||||
+ static int output;
|
||||
+ std::shared_ptr<wgpu::SurfaceSourceSwitchNWindow> desc = std::make_shared<wgpu::SurfaceSourceSwitchNWindow>();
|
||||
+ desc->window = &output;
|
||||
+ return std::move(desc);
|
||||
#elif defined(SDL_PLATFORM_LINUX)
|
||||
const char* driver = SDL_GetCurrentVideoDriver();
|
||||
if (SDL_strcmp(driver, "wayland") == 0) {
|
||||
@@ -0,0 +1,15 @@
|
||||
diff --git a/absl/base/internal/raw_logging.cc b/absl/base/internal/raw_logging.cc
|
||||
index 8537f3e..ce7b92f 100644
|
||||
--- a/absl/base/internal/raw_logging.cc
|
||||
+++ b/absl/base/internal/raw_logging.cc
|
||||
@@ -56,7 +56,9 @@
|
||||
// ABSL_HAVE_SYSCALL_WRITE is defined when the platform provides the syscall
|
||||
// syscall(SYS_write, /*int*/ fd, /*char* */ buf, /*size_t*/ len);
|
||||
// for low level operations that want to avoid libc.
|
||||
-#if (defined(__linux__) || defined(__FreeBSD__)) && !defined(__ANDROID__)
|
||||
+// The PS5 (__PROSPERO__) has write() but no syscall() a title can import.
|
||||
+#if (defined(__linux__) || defined(__FreeBSD__)) && !defined(__ANDROID__) && \
|
||||
+ !defined(__PROSPERO__)
|
||||
#include <sys/syscall.h>
|
||||
#define ABSL_HAVE_SYSCALL_WRITE 1
|
||||
#define ABSL_LOW_LEVEL_WRITE_SUPPORTED 1
|
||||
@@ -0,0 +1,229 @@
|
||||
diff --git a/src/dawn/common/SystemUtils.cpp b/src/dawn/common/SystemUtils.cpp
|
||||
index df995f32a..87da23cc1 100644
|
||||
--- a/src/dawn/common/SystemUtils.cpp
|
||||
+++ b/src/dawn/common/SystemUtils.cpp
|
||||
@@ -47,7 +47,7 @@
|
||||
#include <mach-o/dyld.h>
|
||||
|
||||
#include <vector>
|
||||
-#elif DAWN_PLATFORM_IS(SWITCH)
|
||||
+#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
#include <cstdlib>
|
||||
#endif
|
||||
|
||||
@@ -161,7 +161,7 @@ std::optional<std::string> GetExecutablePath() {
|
||||
std::optional<std::string> GetExecutablePath() {
|
||||
return {};
|
||||
}
|
||||
-#elif DAWN_PLATFORM_IS(SWITCH)
|
||||
+#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
std::optional<std::string> GetExecutablePath() {
|
||||
return {};
|
||||
}
|
||||
@@ -219,7 +219,7 @@ std::optional<std::string> GetModulePath() {
|
||||
std::optional<std::string> GetModulePath() {
|
||||
return {};
|
||||
}
|
||||
-#elif DAWN_PLATFORM_IS(SWITCH)
|
||||
+#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
std::optional<std::string> GetModulePath() {
|
||||
return {};
|
||||
}
|
||||
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
|
||||
index f45b4fa1e..d019ff298 100644
|
||||
--- a/src/dawn/native/CMakeLists.txt
|
||||
+++ b/src/dawn/native/CMakeLists.txt
|
||||
@@ -869,7 +869,7 @@ if (DAWN_ENABLE_VULKAN)
|
||||
"vulkan/external_memory/MemoryServiceImplementationAHardwareBuffer.cpp"
|
||||
"vulkan/external_semaphore/SemaphoreServiceImplementationFD.cpp"
|
||||
)
|
||||
- elseif (UNIX AND NOT APPLE)
|
||||
+ elseif (UNIX AND NOT APPLE AND NOT DAWN_PLATFORM_PS5)
|
||||
list(APPEND private_headers
|
||||
"vulkan/external_memory/MemoryServiceImplementationDmaBuf.h"
|
||||
"vulkan/external_memory/MemoryServiceImplementationOpaqueFD.h"
|
||||
diff --git a/src/dawn/native/Surface.cpp b/src/dawn/native/Surface.cpp
|
||||
index 8ebe3104c..6eefeee29 100644
|
||||
--- a/src/dawn/native/Surface.cpp
|
||||
+++ b/src/dawn/native/Surface.cpp
|
||||
@@ -181,7 +181,7 @@ ResultOrError<UnpackedPtr<SurfaceDescriptor>> ValidateSurfaceDescriptor(
|
||||
return descriptor;
|
||||
}
|
||||
#endif // defined(DAWN_USE_WINDOWS_UI)
|
||||
-#if DAWN_PLATFORM_IS(SWITCH)
|
||||
+#if DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
case wgpu::SType::SurfaceSourceSwitchNWindow: {
|
||||
auto* subDesc = descriptor.Get<SurfaceSourceSwitchNWindow>();
|
||||
DAWN_ASSERT(subDesc != nullptr);
|
||||
diff --git a/src/dawn/native/vulkan/BackendVk.cpp b/src/dawn/native/vulkan/BackendVk.cpp
|
||||
index 064b572f5..9b8b0f780 100644
|
||||
--- a/src/dawn/native/vulkan/BackendVk.cpp
|
||||
+++ b/src/dawn/native/vulkan/BackendVk.cpp
|
||||
@@ -69,7 +69,7 @@ constexpr char kVulkanLibName[] = "vulkan-1.dll";
|
||||
constexpr char kVulkanLibName[] = "libvulkan.dylib";
|
||||
#elif DAWN_PLATFORM_IS(FUCHSIA)
|
||||
constexpr char kVulkanLibName[] = "libvulkan.so";
|
||||
-#elif DAWN_PLATFORM_IS(SWITCH)
|
||||
+#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
constexpr char kVulkanLibName[] = "";
|
||||
#else
|
||||
#error "Unimplemented Vulkan backend platform"
|
||||
diff --git a/src/dawn/native/vulkan/SwapChainVk.cpp b/src/dawn/native/vulkan/SwapChainVk.cpp
|
||||
index 179453e57..5b3623f8e 100644
|
||||
--- a/src/dawn/native/vulkan/SwapChainVk.cpp
|
||||
+++ b/src/dawn/native/vulkan/SwapChainVk.cpp
|
||||
@@ -687,6 +687,46 @@ ResultOrError<VkSurfaceKHR> CreateVulkanSurface(InstanceBase* instance,
|
||||
}
|
||||
#endif // DAWN_PLATFORM_IS(SWITCH)
|
||||
|
||||
+#if DAWN_PLATFORM_IS(PS5)
|
||||
+ case Surface::Type::SwitchNWindow: {
|
||||
+ // The console's VideoOut, as VK_KHR_display's first display in its first mode.
|
||||
+ if (info.HasExt(InstanceExt::Display)) {
|
||||
+ VkPhysicalDevice vkPhysicalDevice = physicalDevice->GetVkPhysicalDevice();
|
||||
+ uint32_t count = 1;
|
||||
+ VkDisplayPropertiesKHR display = {};
|
||||
+ VkResult result = VkResult::WrapUnsafe(fn.GetPhysicalDeviceDisplayPropertiesKHR(
|
||||
+ vkPhysicalDevice, &count, &display));
|
||||
+ if ((result != VK_SUCCESS && result != VK_INCOMPLETE) || count == 0) {
|
||||
+ return DAWN_INTERNAL_ERROR("GetPhysicalDeviceDisplayPropertiesKHR");
|
||||
+ }
|
||||
+ count = 1;
|
||||
+ VkDisplayModePropertiesKHR mode = {};
|
||||
+ result = VkResult::WrapUnsafe(fn.GetDisplayModePropertiesKHR(
|
||||
+ vkPhysicalDevice, display.display, &count, &mode));
|
||||
+ if ((result != VK_SUCCESS && result != VK_INCOMPLETE) || count == 0) {
|
||||
+ return DAWN_INTERNAL_ERROR("GetDisplayModePropertiesKHR");
|
||||
+ }
|
||||
+
|
||||
+ VkDisplaySurfaceCreateInfoKHR createInfo = {};
|
||||
+ createInfo.sType = VK_STRUCTURE_TYPE_DISPLAY_SURFACE_CREATE_INFO_KHR;
|
||||
+ createInfo.displayMode = mode.displayMode;
|
||||
+ createInfo.planeIndex = 0;
|
||||
+ createInfo.planeStackIndex = 0;
|
||||
+ createInfo.transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
+ createInfo.globalAlpha = 1.0f;
|
||||
+ createInfo.alphaMode = VK_DISPLAY_PLANE_ALPHA_OPAQUE_BIT_KHR;
|
||||
+ createInfo.imageExtent = mode.parameters.visibleRegion;
|
||||
+
|
||||
+ VkSurfaceKHR vkSurface = VK_NULL_HANDLE;
|
||||
+ DAWN_TRY(CheckVkSuccess(
|
||||
+ fn.CreateDisplayPlaneSurfaceKHR(vkInstance, &createInfo, nullptr, &*vkSurface),
|
||||
+ "CreateDisplayPlaneSurfaceKHR"));
|
||||
+ return vkSurface;
|
||||
+ }
|
||||
+ break;
|
||||
+ }
|
||||
+#endif // DAWN_PLATFORM_IS(PS5)
|
||||
+
|
||||
#if defined(DAWN_USE_WAYLAND)
|
||||
case Surface::Type::WaylandSurface: {
|
||||
if (info.HasExt(InstanceExt::WaylandSurface)) {
|
||||
diff --git a/src/dawn/native/vulkan/VulkanExtensions.cpp b/src/dawn/native/vulkan/VulkanExtensions.cpp
|
||||
index a327a83de..a3496b7c2 100644
|
||||
--- a/src/dawn/native/vulkan/VulkanExtensions.cpp
|
||||
+++ b/src/dawn/native/vulkan/VulkanExtensions.cpp
|
||||
@@ -50,6 +50,7 @@ static constexpr std::array<InstanceExtInfo, kInstanceExtCount> sInstanceExtInfo
|
||||
{InstanceExt::XlibSurface, "VK_KHR_xlib_surface"},
|
||||
{InstanceExt::AndroidSurface, "VK_KHR_android_surface"},
|
||||
{InstanceExt::ViSurface, "VK_NN_vi_surface"},
|
||||
+ {InstanceExt::Display, "VK_KHR_display"},
|
||||
|
||||
{InstanceExt::DebugUtils, "VK_EXT_debug_utils"},
|
||||
{InstanceExt::ValidationFeatures, "VK_EXT_validation_features"},
|
||||
@@ -104,6 +105,7 @@ InstanceExtSet EnsureDependencies(const InstanceExtSet& advertisedExts) {
|
||||
case InstanceExt::XcbSurface:
|
||||
case InstanceExt::XlibSurface:
|
||||
case InstanceExt::ViSurface:
|
||||
+ case InstanceExt::Display:
|
||||
hasDependencies = HasDep(InstanceExt::Surface);
|
||||
break;
|
||||
|
||||
diff --git a/src/dawn/native/vulkan/VulkanExtensions.h b/src/dawn/native/vulkan/VulkanExtensions.h
|
||||
index 66d304128..a04f41d78 100644
|
||||
--- a/src/dawn/native/vulkan/VulkanExtensions.h
|
||||
+++ b/src/dawn/native/vulkan/VulkanExtensions.h
|
||||
@@ -48,6 +48,7 @@ enum class InstanceExt : uint32_t {
|
||||
XlibSurface,
|
||||
AndroidSurface,
|
||||
ViSurface,
|
||||
+ Display,
|
||||
|
||||
// Others
|
||||
DebugUtils,
|
||||
diff --git a/src/dawn/native/vulkan/VulkanFunctions.cpp b/src/dawn/native/vulkan/VulkanFunctions.cpp
|
||||
index 1dba51978..c9db13b01 100644
|
||||
--- a/src/dawn/native/vulkan/VulkanFunctions.cpp
|
||||
+++ b/src/dawn/native/vulkan/VulkanFunctions.cpp
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
namespace dawn::native::vulkan {
|
||||
|
||||
-#if DAWN_PLATFORM_IS(SWITCH)
|
||||
+#if DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
extern "C" {
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vk_icdGetInstanceProcAddr(VkInstance instance, const char* pName);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ F AsVkFn(void(VKAPI_PTR* addr)()) {
|
||||
} while (0)
|
||||
|
||||
MaybeError VulkanFunctions::LoadGlobalProcs(const DynamicLib& vulkanLib) {
|
||||
-#if DAWN_PLATFORM_IS(SWITCH)
|
||||
+#if DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5)
|
||||
(void)vulkanLib;
|
||||
GetInstanceProcAddr = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
|
||||
vk_icdGetInstanceProcAddr(VK_NULL_HANDLE, "vkGetInstanceProcAddr"));
|
||||
@@ -233,6 +233,12 @@ MaybeError VulkanFunctions::LoadInstanceProcs(VkInstance instance,
|
||||
}
|
||||
#endif // defined(DAWN_USE_X11)
|
||||
|
||||
+ if (globalInfo.HasExt(InstanceExt::Display)) {
|
||||
+ GET_INSTANCE_PROC(GetPhysicalDeviceDisplayPropertiesKHR);
|
||||
+ GET_INSTANCE_PROC(GetDisplayModePropertiesKHR);
|
||||
+ GET_INSTANCE_PROC(CreateDisplayPlaneSurfaceKHR);
|
||||
+ }
|
||||
+
|
||||
#if defined(VK_USE_PLATFORM_VI_NN)
|
||||
if (globalInfo.HasExt(InstanceExt::ViSurface)) {
|
||||
GET_INSTANCE_PROC(CreateViSurfaceNN);
|
||||
diff --git a/src/dawn/native/vulkan/VulkanFunctions.h b/src/dawn/native/vulkan/VulkanFunctions.h
|
||||
index e61a0ba2c..4f8a84e9f 100644
|
||||
--- a/src/dawn/native/vulkan/VulkanFunctions.h
|
||||
+++ b/src/dawn/native/vulkan/VulkanFunctions.h
|
||||
@@ -204,6 +204,11 @@ struct VulkanFunctions {
|
||||
VkFn<PFN_vkCreateViSurfaceNN> CreateViSurfaceNN = nullptr;
|
||||
#endif // defined(VK_USE_PLATFORM_VI_NN)
|
||||
|
||||
+ // KHR_display, the PS5's only surface (VideoOut)
|
||||
+ VkFn<PFN_vkGetPhysicalDeviceDisplayPropertiesKHR> GetPhysicalDeviceDisplayPropertiesKHR = nullptr;
|
||||
+ VkFn<PFN_vkGetDisplayModePropertiesKHR> GetDisplayModePropertiesKHR = nullptr;
|
||||
+ VkFn<PFN_vkCreateDisplayPlaneSurfaceKHR> CreateDisplayPlaneSurfaceKHR = nullptr;
|
||||
+
|
||||
// ---------- Instance procs for device extensions
|
||||
|
||||
// VK_KHR_cooperative_matrix
|
||||
diff --git a/src/utils/platform.h b/src/utils/platform.h
|
||||
index 9eee1aa91..aacc4c248 100644
|
||||
--- a/src/utils/platform.h
|
||||
+++ b/src/utils/platform.h
|
||||
@@ -99,6 +99,10 @@
|
||||
#elif defined(__SWITCH__)
|
||||
#define DAWN_PLATFORM_IS_SWITCH 1
|
||||
|
||||
+#elif defined(__PROSPERO__)
|
||||
+#define DAWN_PLATFORM_IS_PS5 1
|
||||
+#define DAWN_PLATFORM_IS_POSIX 1
|
||||
+
|
||||
#else
|
||||
#error "Unsupported platform."
|
||||
#endif
|
||||
@@ -248,6 +252,9 @@ static_assert(sizeof(sizeof(char)) == 4, "Expect sizeof(size_t) == 4");
|
||||
#if !defined(DAWN_PLATFORM_IS_SWITCH)
|
||||
#define DAWN_PLATFORM_IS_SWITCH 0
|
||||
#endif
|
||||
+#if !defined(DAWN_PLATFORM_IS_PS5)
|
||||
+#define DAWN_PLATFORM_IS_PS5 0
|
||||
+#endif
|
||||
|
||||
#if !defined(DAWN_PLATFORM_IS_X86)
|
||||
#define DAWN_PLATFORM_IS_X86 0
|
||||
@@ -0,0 +1,165 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index faf6d90..a3b860c 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -1329,6 +1329,31 @@ if(SDL_HAPTIC)
|
||||
endif()
|
||||
|
||||
|
||||
+# The PlayStation 5 (the payload SDK's toolchain sets PS5): a FreeBSD-like
|
||||
+# system for threads, time and files, with the console's own video, audio and
|
||||
+# pad drivers, kept outside this tree in SDL_PS5_PORT_DIR.
|
||||
+if(PS5)
|
||||
+ if(NOT SDL_PS5_PORT_DIR OR NOT SDL_PS5_PLATFORM_DIR)
|
||||
+ message(FATAL_ERROR "PS5: set SDL_PS5_PORT_DIR (the drivers) and SDL_PS5_PLATFORM_DIR (platform.h)")
|
||||
+ endif()
|
||||
+ sdl_include_directories(PRIVATE "${SDL_PS5_PLATFORM_DIR}")
|
||||
+ if(SDL_AUDIO)
|
||||
+ set(SDL_AUDIO_DRIVER_PS5 1)
|
||||
+ sdl_sources("${SDL_PS5_PORT_DIR}/SDL_ps5audio.c")
|
||||
+ set(HAVE_SDL_AUDIO TRUE)
|
||||
+ endif()
|
||||
+ if(SDL_JOYSTICK)
|
||||
+ set(SDL_JOYSTICK_PS5 1)
|
||||
+ sdl_sources("${SDL_PS5_PORT_DIR}/SDL_ps5joystick.c")
|
||||
+ set(HAVE_SDL_JOYSTICK TRUE)
|
||||
+ endif()
|
||||
+ if(SDL_VIDEO)
|
||||
+ set(SDL_VIDEO_DRIVER_PS5 1)
|
||||
+ sdl_sources("${SDL_PS5_PORT_DIR}/SDL_ps5video.c")
|
||||
+ set(HAVE_SDL_VIDEO TRUE)
|
||||
+ endif()
|
||||
+endif()
|
||||
+
|
||||
# General SDL subsystem options, valid for all platforms
|
||||
if(SDL_AUDIO)
|
||||
# CheckDummyAudio/CheckDiskAudio - valid for all platforms
|
||||
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
|
||||
index 17cb8f2..0cb0934 100644
|
||||
--- a/include/build_config/SDL_build_config.h.cmake
|
||||
+++ b/include/build_config/SDL_build_config.h.cmake
|
||||
@@ -288,6 +288,7 @@
|
||||
#cmakedefine SDL_AUDIO_DRIVER_VITA 1
|
||||
#cmakedefine SDL_AUDIO_DRIVER_PSP 1
|
||||
#cmakedefine SDL_AUDIO_DRIVER_PS2 1
|
||||
+#cmakedefine SDL_AUDIO_DRIVER_PS5 1
|
||||
#cmakedefine SDL_AUDIO_DRIVER_N3DS 1
|
||||
#cmakedefine SDL_AUDIO_DRIVER_NGAGE 1
|
||||
#cmakedefine SDL_AUDIO_DRIVER_QNX 1
|
||||
@@ -312,6 +313,7 @@
|
||||
#cmakedefine SDL_JOYSTICK_MFI 1
|
||||
#cmakedefine SDL_JOYSTICK_N3DS 1
|
||||
#cmakedefine SDL_JOYSTICK_PS2 1
|
||||
+#cmakedefine SDL_JOYSTICK_PS5 1
|
||||
#cmakedefine SDL_JOYSTICK_PSP 1
|
||||
#cmakedefine SDL_JOYSTICK_RAWINPUT 1
|
||||
#cmakedefine SDL_JOYSTICK_USBHID 1
|
||||
@@ -407,6 +409,7 @@
|
||||
#cmakedefine SDL_VIDEO_DRIVER_NGAGE 1
|
||||
#cmakedefine SDL_VIDEO_DRIVER_OFFSCREEN 1
|
||||
#cmakedefine SDL_VIDEO_DRIVER_PS2 1
|
||||
+#cmakedefine SDL_VIDEO_DRIVER_PS5 1
|
||||
#cmakedefine SDL_VIDEO_DRIVER_PSP 1
|
||||
#cmakedefine SDL_VIDEO_DRIVER_RISCOS 1
|
||||
#cmakedefine SDL_VIDEO_DRIVER_ROCKCHIP 1
|
||||
diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c
|
||||
index c1e4241..b47a897 100644
|
||||
--- a/src/audio/SDL_audio.c
|
||||
+++ b/src/audio/SDL_audio.c
|
||||
@@ -65,6 +65,9 @@ static const AudioBootStrap *const bootstrap[] = {
|
||||
#ifdef SDL_AUDIO_DRIVER_OPENSLES
|
||||
&OPENSLES_bootstrap,
|
||||
#endif
|
||||
+#ifdef SDL_AUDIO_DRIVER_PS5
|
||||
+ &PS5AUDIO_bootstrap,
|
||||
+#endif
|
||||
#ifdef SDL_AUDIO_DRIVER_PS2
|
||||
&PS2AUDIO_bootstrap,
|
||||
#endif
|
||||
diff --git a/src/audio/SDL_sysaudio.h b/src/audio/SDL_sysaudio.h
|
||||
index 9104be8..fea29a1 100644
|
||||
--- a/src/audio/SDL_sysaudio.h
|
||||
+++ b/src/audio/SDL_sysaudio.h
|
||||
@@ -384,6 +384,7 @@ extern AudioBootStrap DUMMYAUDIO_bootstrap;
|
||||
extern AudioBootStrap AAUDIO_bootstrap;
|
||||
extern AudioBootStrap OPENSLES_bootstrap;
|
||||
extern AudioBootStrap PS2AUDIO_bootstrap;
|
||||
+extern AudioBootStrap PS5AUDIO_bootstrap;
|
||||
extern AudioBootStrap PSPAUDIO_bootstrap;
|
||||
extern AudioBootStrap VITAAUD_bootstrap;
|
||||
extern AudioBootStrap N3DSAUDIO_bootstrap;
|
||||
diff --git a/src/filesystem/unix/SDL_sysfilesystem.c b/src/filesystem/unix/SDL_sysfilesystem.c
|
||||
index a23d67b..1786134 100644
|
||||
--- a/src/filesystem/unix/SDL_sysfilesystem.c
|
||||
+++ b/src/filesystem/unix/SDL_sysfilesystem.c
|
||||
@@ -240,6 +240,10 @@ static char *GetExePath(void)
|
||||
|
||||
char *SDL_SYS_GetBasePath(void)
|
||||
{
|
||||
+#ifdef __PROSPERO__
|
||||
+ // PlayStation 5: a title's own folder is mounted at /app0
|
||||
+ return SDL_strdup("/app0/");
|
||||
+#else
|
||||
char *path = GetExePath();
|
||||
if (!path) {
|
||||
return NULL;
|
||||
@@ -252,6 +256,7 @@ char *SDL_SYS_GetBasePath(void)
|
||||
|
||||
ptr = (char *) SDL_realloc(path, ((size_t) (ptr - path)) + 2); // try to shrink this allocation down a little.
|
||||
return ptr ? ptr : path; // return shrunk buffer if shrink worked out, unchanged original buffer if not.
|
||||
+#endif // __PROSPERO__
|
||||
}
|
||||
|
||||
|
||||
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
|
||||
index 02cc012..ac2affe 100644
|
||||
--- a/src/joystick/SDL_joystick.c
|
||||
+++ b/src/joystick/SDL_joystick.c
|
||||
@@ -91,6 +91,9 @@ static SDL_JoystickDriver *SDL_joystick_drivers[] = {
|
||||
#ifdef SDL_JOYSTICK_USBHID /* !!! FIXME: "USBHID" is a generic name, and doubly-confusing with HIDAPI next to it. This is the *BSD interface, rename this. */
|
||||
&SDL_BSD_JoystickDriver,
|
||||
#endif
|
||||
+#ifdef SDL_JOYSTICK_PS5
|
||||
+ &SDL_PS5_JoystickDriver,
|
||||
+#endif
|
||||
#ifdef SDL_JOYSTICK_PS2
|
||||
&SDL_PS2_JoystickDriver,
|
||||
#endif
|
||||
diff --git a/src/joystick/SDL_sysjoystick.h b/src/joystick/SDL_sysjoystick.h
|
||||
index 47ce42c..833d9a4 100644
|
||||
--- a/src/joystick/SDL_sysjoystick.h
|
||||
+++ b/src/joystick/SDL_sysjoystick.h
|
||||
@@ -259,6 +259,7 @@ extern SDL_JoystickDriver SDL_WGI_JoystickDriver;
|
||||
extern SDL_JoystickDriver SDL_WINDOWS_JoystickDriver;
|
||||
extern SDL_JoystickDriver SDL_WINMM_JoystickDriver;
|
||||
extern SDL_JoystickDriver SDL_PS2_JoystickDriver;
|
||||
+extern SDL_JoystickDriver SDL_PS5_JoystickDriver;
|
||||
extern SDL_JoystickDriver SDL_PSP_JoystickDriver;
|
||||
extern SDL_JoystickDriver SDL_VITA_JoystickDriver;
|
||||
extern SDL_JoystickDriver SDL_N3DS_JoystickDriver;
|
||||
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
|
||||
index 1fdc5bf..4189995 100644
|
||||
--- a/src/video/SDL_sysvideo.h
|
||||
+++ b/src/video/SDL_sysvideo.h
|
||||
@@ -530,6 +530,7 @@ extern VideoBootStrap HAIKU_bootstrap;
|
||||
extern VideoBootStrap UIKIT_bootstrap;
|
||||
extern VideoBootStrap Android_bootstrap;
|
||||
extern VideoBootStrap PS2_bootstrap;
|
||||
+extern VideoBootStrap PS5_bootstrap;
|
||||
extern VideoBootStrap PSP_bootstrap;
|
||||
extern VideoBootStrap VITA_bootstrap;
|
||||
extern VideoBootStrap RISCOS_bootstrap;
|
||||
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
|
||||
index 48ed85a..1c86bf5 100644
|
||||
--- a/src/video/SDL_video.c
|
||||
+++ b/src/video/SDL_video.c
|
||||
@@ -116,6 +116,9 @@ static VideoBootStrap *bootstrap[] = {
|
||||
#ifdef SDL_VIDEO_DRIVER_ANDROID
|
||||
&Android_bootstrap,
|
||||
#endif
|
||||
+#ifdef SDL_VIDEO_DRIVER_PS5
|
||||
+ &PS5_bootstrap,
|
||||
+#endif
|
||||
#ifdef SDL_VIDEO_DRIVER_PS2
|
||||
&PS2_bootstrap,
|
||||
#endif
|
||||
@@ -0,0 +1,374 @@
|
||||
diff --git a/native/src/pc/features/pc_menu.cpp b/native/src/pc/features/pc_menu.cpp
|
||||
index 3a26b60..c02f46c 100644
|
||||
--- a/native/src/pc/features/pc_menu.cpp
|
||||
+++ b/native/src/pc/features/pc_menu.cpp
|
||||
@@ -144,6 +144,10 @@ uint32_t readGamepads() {
|
||||
// Minus alone: the SDL shim shows every controller as a Pro Controller, whose mapping gives
|
||||
// Minus (BACK) to no game button.
|
||||
bool combo = SDL_GetGamepadButton(gp, SDL_GAMEPAD_BUTTON_BACK);
|
||||
+#elif defined(__PROSPERO__)
|
||||
+ // PlayStation 5: the touch pad's click, which the port's SDL driver reports as BACK and
|
||||
+ // the game's default mapping gives to no button.
|
||||
+ bool combo = SDL_GetGamepadButton(gp, SDL_GAMEPAD_BUTTON_BACK);
|
||||
#else
|
||||
const bool lt = SDL_GetGamepadAxis(gp, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > 16000;
|
||||
const bool rt = SDL_GetGamepadAxis(gp, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > 16000;
|
||||
@@ -448,7 +452,14 @@ const PcSettingChoice kFbScale[] = {
|
||||
#endif
|
||||
{"1.0", "854x480 (1.0)"}, {"1.125", "960x540 (1.125)"}, {"1.25", "1067x600 (1.25)"},
|
||||
{"1.5", "1280x720 (1.5)"}, {"2.0", "1707x960 (2.0)"}, {"2.25", "1920x1080 (2.25)"},
|
||||
+#if defined(__PROSPERO__)
|
||||
+ {"3.0", "2560x1440 (3.0)"}, {"4.5", "3840x2160 (4K, 4.5)"},
|
||||
+#endif
|
||||
};
|
||||
+#if defined(__PROSPERO__)
|
||||
+// PlayStation 5: the size of the picture the port hands to the console (ps5/src/startup.c reads it)
|
||||
+const PcSettingChoice kPs5Output[] = {{"2160", "3840x2160 (4K)"}, {"1440", "2560x1440"}, {"1080", "1920x1080"}};
|
||||
+#endif
|
||||
const PcSettingChoice kAspect[] = {{"16:9", "16:9 (panorámica)", "16:9 (widescreen)"}, {"4:3", "4:3 (GameCube)"}, {"16:10", "16:10"}};
|
||||
const PcSettingChoice kOnOff[] = {{"0", "Desactivado", "Off"}, {"1", "Activado", "On"}};
|
||||
const PcSettingChoice kHdMaxSize[] = {{"auto", "Automático (512 / 1024)", "Automatic (512 / 1024)"}, {"256", "256"}, {"512", "512"},
|
||||
@@ -528,6 +539,13 @@ const PcSettingDesc kBuiltins[] = {
|
||||
70,
|
||||
"Aspect ratio",
|
||||
"16:9 widescreen or the GameCube's original 4:3."},
|
||||
+#if defined(__PROSPERO__)
|
||||
+ {"COS_PS5_OUTPUT", "Resolución de salida",
|
||||
+ "Tamaño de la imagen que el juego entrega a la consola. La resolución interna «Tamaño de la ventana» es esta.",
|
||||
+ PC_SETTING_TAB_GRAPHICS, PC_SETTING_RESTART, CHOICES(kPs5Output), "2160", nullptr, nullptr, 71,
|
||||
+ "Output resolution",
|
||||
+ "The size of the picture the game hands to the console. The internal resolution \"Window size\" is this one."},
|
||||
+#endif
|
||||
{"COS_HD_TEXTURES", "Texturas HD", "Usa el paquete de texturas en alta resolución si está instalado.",
|
||||
PC_SETTING_TAB_GRAPHICS, 0, CHOICES(kOnOff), "0", nullptr, nullptr, 80,
|
||||
"HD textures",
|
||||
diff --git a/native/src/pc/features/pc_overlay.cpp b/native/src/pc/features/pc_overlay.cpp
|
||||
index 43728f2..b3ce94c 100644
|
||||
--- a/native/src/pc/features/pc_overlay.cpp
|
||||
+++ b/native/src/pc/features/pc_overlay.cpp
|
||||
@@ -13,6 +13,7 @@
|
||||
// per frame of the frames read back in the half second (GL_TIME_ELAPSED_EXT, a few frames late).
|
||||
#include "pc_internal.h"
|
||||
|
||||
+#include <algorithm>
|
||||
#include <imgui.h>
|
||||
|
||||
#if defined(__SWITCH__)
|
||||
@@ -110,12 +111,19 @@ void overlayFrame(uint64_t busyNs, bool gameFrame) {
|
||||
overlayUpdate(now);
|
||||
}
|
||||
|
||||
- ImGui::SetNextWindowPos(ImVec2(8.0f, 8.0f), ImGuiCond_Always);
|
||||
+#if defined(__PROSPERO__)
|
||||
+ // PlayStation 5: a television at up to 3840x2160; the font's own size is for a 720-line window
|
||||
+ const float scale = std::max(1.0f, ImGui::GetIO().DisplaySize.y / 720.0f);
|
||||
+#else
|
||||
+ const float scale = 1.0f;
|
||||
+#endif
|
||||
+ ImGui::SetNextWindowPos(ImVec2(8.0f * scale, 8.0f * scale), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowBgAlpha(0.55f);
|
||||
constexpr ImGuiWindowFlags kFlags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs;
|
||||
if (ImGui::Begin("##cos_fps_overlay", nullptr, kFlags)) {
|
||||
+ ImGui::SetWindowFontScale(scale);
|
||||
if (!s.measured) {
|
||||
// Turned on a moment ago (options menu): nothing measured yet.
|
||||
ImGui::TextUnformatted("-- fps");
|
||||
diff --git a/native/src/pc/harness/pc_shot.cpp b/native/src/pc/harness/pc_shot.cpp
|
||||
index aef4e19..c2c3de0 100644
|
||||
--- a/native/src/pc/harness/pc_shot.cpp
|
||||
+++ b/native/src/pc/harness/pc_shot.cpp
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
+#include <sys/stat.h>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
@@ -568,7 +569,48 @@ void debugShotFrameEnd(unsigned int frame) {
|
||||
sDebugShotOk = ok ? ticket : sDebugShotOk;
|
||||
}
|
||||
|
||||
+#if defined(__PROSPERO__)
|
||||
+// PlayStation 5: PS5 DevLoop's grab and quit (PS5_DevLoop/AGENTS.md, "Giving a title grab"). Once
|
||||
+// every few frames the title tries to remove a request file in its own folder: removing it is the
|
||||
+// check, one system call that fails on every ordinary launch. devloop-shot: this frame's picture,
|
||||
+// with the options menu and overlays, becomes a new PNG in a folder that holds nothing else.
|
||||
+// devloop-quit: a clean exit.
|
||||
+void ps5DevLoop(unsigned int frame) {
|
||||
+ static unsigned int grabs = 0;
|
||||
+ if (frame % 4 != 0) {
|
||||
+ return;
|
||||
+ }
|
||||
+ if (unlink("/app0/devloop-quit") == 0) {
|
||||
+ writef(STDERR_FILENO, "[cos] ps5: devloop-quit\n");
|
||||
+ pc_exit(PC_EXIT_REACHED);
|
||||
+ }
|
||||
+ if (unlink("/app0/devloop-shot") != 0) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const char* partial = "/app0/user/grab.partial";
|
||||
+ mkdir("/app0/user/screenshots", 0777);
|
||||
+ chmod("/app0/user", 0777);
|
||||
+ chmod("/app0/user/screenshots", 0777);
|
||||
+ {
|
||||
+ std::lock_guard<std::mutex> lk(sDebugShotMu);
|
||||
+ sDebugShotPath = partial;
|
||||
+ sDebugShotOverlay = true;
|
||||
+ sDebugShotAsked++;
|
||||
+ }
|
||||
+ debugShotFrameEnd(frame);
|
||||
+ char name[64];
|
||||
+ snprintf(name, sizeof(name), "/app0/user/screenshots/grab-%04u.png", grabs++);
|
||||
+ chmod(partial, 0666);
|
||||
+ if (rename(partial, name) != 0) {
|
||||
+ writef(STDERR_FILENO, "[cos] ps5: grab: no picture written\n");
|
||||
+ }
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
void shotFrameEnd(unsigned int frame) {
|
||||
+#if defined(__PROSPERO__)
|
||||
+ ps5DevLoop(frame);
|
||||
+#endif
|
||||
debugShotFrameEnd(frame);
|
||||
if (!sShotOn || !wanted(frame)) {
|
||||
return;
|
||||
diff --git a/native/src/pc/runtime/pc_crash.cpp b/native/src/pc/runtime/pc_crash.cpp
|
||||
index 094f649..48d32d3 100644
|
||||
--- a/native/src/pc/runtime/pc_crash.cpp
|
||||
+++ b/native/src/pc/runtime/pc_crash.cpp
|
||||
@@ -221,6 +221,9 @@ void writeFrames(int fd, void** frames, int n, int exact) {
|
||||
void writeThreadName(int fd) {
|
||||
#if defined(__SWITCH__)
|
||||
writef(fd, "[cos] thread %llu\n", (unsigned long long)cos_switch_thread_id());
|
||||
+#elif defined(__PROSPERO__)
|
||||
+ // PlayStation 5: the console's libc has no pthread_getname_np
|
||||
+ writef(fd, "[cos] thread %p\n", (void*)pthread_self());
|
||||
#else
|
||||
char name[64] = "";
|
||||
pthread_getname_np(pthread_self(), name, sizeof(name));
|
||||
@@ -399,6 +402,10 @@ void installCrashHandler() {
|
||||
// libnx's exception handler (switch/native/source/cos_switch.cpp) reports crashes; it adds
|
||||
// this state line.
|
||||
cos_switch_set_crash_state_writer(writeState);
|
||||
+#elif defined(__PROSPERO__)
|
||||
+ // PlayStation 5: no handler. The console writes a crash record of its own to klog (registers
|
||||
+ // and a frame-pointer walk), which the port's ps5/tools/run.sh symbolises; a handler here
|
||||
+ // would take the signal first and exit without one.
|
||||
#else
|
||||
// Alternate signal stack for this (the main) thread, so a stack overflow still reports.
|
||||
static char sAltStack[256 * 1024];
|
||||
diff --git a/native/src/pc/runtime/pc_disc.cpp b/native/src/pc/runtime/pc_disc.cpp
|
||||
index 8d3e3b5..3651d6c 100644
|
||||
--- a/native/src/pc/runtime/pc_disc.cpp
|
||||
+++ b/native/src/pc/runtime/pc_disc.cpp
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cerrno>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
+#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -76,8 +77,30 @@ int checkDisc() {
|
||||
return PC_EXIT_DISC;
|
||||
}
|
||||
if (memcmp(header, "CISO", 4) == 0) {
|
||||
- discMessage("%s is a CISO image; convert it to a plain .iso (decision H9)", path);
|
||||
- return PC_EXIT_DISC;
|
||||
+ // The PS5 port reads CISO images (its nod, ps5/nod, does; so does the desktop hosts' nod,
|
||||
+ // with COS_ALLOW_CISO=1 to try it there): the disc's own header is the first block, which
|
||||
+ // follows the 0x8000-byte block map. Decision H9 stands elsewhere (the Switch's reader
|
||||
+ // takes plain images only).
|
||||
+#if defined(__PROSPERO__)
|
||||
+ bool ciso = true;
|
||||
+#else
|
||||
+ const char* allow = getenv("COS_ALLOW_CISO");
|
||||
+ bool ciso = allow != nullptr && strcmp(allow, "1") == 0;
|
||||
+#endif
|
||||
+ if (ciso) {
|
||||
+ ciso = header[8] != 0; // the first block must be present
|
||||
+ fd = ciso ? open(path, O_RDONLY | O_CLOEXEC) : -1;
|
||||
+ if (fd >= 0) {
|
||||
+ ciso = pread(fd, header, sizeof(header), 0x8000) == (ssize_t)sizeof(header);
|
||||
+ close(fd);
|
||||
+ } else {
|
||||
+ ciso = false;
|
||||
+ }
|
||||
+ }
|
||||
+ if (!ciso) {
|
||||
+ discMessage("%s is a CISO image; convert it to a plain .iso (decision H9)", path);
|
||||
+ return PC_EXIT_DISC;
|
||||
+ }
|
||||
}
|
||||
const uint32_t magic = ((uint32_t)header[0x1C] << 24) | ((uint32_t)header[0x1D] << 16) |
|
||||
((uint32_t)header[0x1E] << 8) | (uint32_t)header[0x1F];
|
||||
diff --git a/native/src/pc/runtime/pc_harness.cpp b/native/src/pc/runtime/pc_harness.cpp
|
||||
index e21ec1b..0291e39 100644
|
||||
--- a/native/src/pc/runtime/pc_harness.cpp
|
||||
+++ b/native/src/pc/runtime/pc_harness.cpp
|
||||
@@ -15,6 +15,10 @@
|
||||
#if defined(__SWITCH__)
|
||||
#include "cos_switch.h"
|
||||
#endif
|
||||
+#if defined(__PROSPERO__)
|
||||
+#include <dirent.h>
|
||||
+#include <strings.h>
|
||||
+#endif
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <pthread.h>
|
||||
@@ -128,11 +132,69 @@ bool executablePath(char* out, size_t size) {
|
||||
}
|
||||
out[n] = '\0';
|
||||
return true;
|
||||
+#elif defined(__PROSPERO__)
|
||||
+ // PlayStation 5: a title's own folder is /app0 (which is /data/homebrew/<TITLE_ID>), the one
|
||||
+ // place it may write: user/ (saves, settings, caches) and the bundled pipeline cache go there.
|
||||
+ snprintf(out, size, "/app0/eboot.bin");
|
||||
+ return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
+#if defined(__PROSPERO__)
|
||||
+namespace {
|
||||
+void ps5Default(const char* name, const char* value) {
|
||||
+ if (getenv(name) == nullptr) {
|
||||
+ setenv(name, value, 0);
|
||||
+ writef(STDERR_FILENO, "[cos] ps5 default: %s=%s\n", name, value);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// There is no file dialog and no command line: the disc is the first .iso, .gcm or .ciso in the
|
||||
+// title's folder, then on a USB drive. checkDisc says what is wrong with it, if anything.
|
||||
+void ps5FindDisc() {
|
||||
+ if (getenv("COS_DISC") != nullptr) {
|
||||
+ return;
|
||||
+ }
|
||||
+ for (const char* dir : {"/app0", "/mnt/usb0", "/mnt/usb1"}) {
|
||||
+ DIR* d = opendir(dir);
|
||||
+ if (d == nullptr) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ char found[512] = "";
|
||||
+ while (const dirent* e = readdir(d)) {
|
||||
+ const char* dot = strrchr(e->d_name, '.');
|
||||
+ if (dot != nullptr && (strcasecmp(dot, ".iso") == 0 || strcasecmp(dot, ".gcm") == 0 ||
|
||||
+ strcasecmp(dot, ".ciso") == 0)) {
|
||||
+ snprintf(found, sizeof(found), "%s/%s", dir, e->d_name);
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ closedir(d);
|
||||
+ if (found[0] != '\0') {
|
||||
+ ps5Default("COS_DISC", found);
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+} // namespace
|
||||
+
|
||||
+// The console's defaults, for what the settings file (the options menu) has not set: as the
|
||||
+// Switch's (switch/native/source/cos_switch.cpp), for a television and a faster machine.
|
||||
+void ps5Defaults() {
|
||||
+ ps5FindDisc();
|
||||
+ ps5Default("COS_RUN_DIR", "/app0/user");
|
||||
+ ps5Default("COS_ASPECT", "16:9");
|
||||
+ // 60 images a second by interpolation (docs/FPS60_PLAN.md); the game's logic stays at 30
|
||||
+ ps5Default("COS_FPS60", "1");
|
||||
+ // the internal resolution is the output's (0: the window's size)
|
||||
+ ps5Default("COS_FB_SCALE", "0");
|
||||
+ // the render pipelines of the bundled list are compiled while the game starts
|
||||
+ ps5Default("COS_PRECOMPILE", "boot");
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
} // namespace pc
|
||||
|
||||
using namespace pc;
|
||||
@@ -145,7 +207,14 @@ void pc_harness_init(int argc, char* argv[]) {
|
||||
sStartNs = monotonicNs();
|
||||
// The options menu's settings file (pc_settings.h) into the environment before anything reads
|
||||
// it (on the Switch cos_switch_start did it already, before its defaults).
|
||||
+#if defined(__PROSPERO__)
|
||||
+ // A television, always: the mode the per-mode settings are read for (before the file is)
|
||||
+ setenv("COS_OPERATION_MODE", "docked", 0);
|
||||
+#endif
|
||||
pc_settings_load_early();
|
||||
+#if defined(__PROSPERO__)
|
||||
+ ps5Defaults();
|
||||
+#endif
|
||||
|
||||
// Line-buffered stdout: the game reports with printf/OSReport into a pipe or file; a watchdog
|
||||
// or crash exit must not lose the last lines in a full buffer.
|
||||
diff --git a/native/src/pc/runtime/pc_main.cpp b/native/src/pc/runtime/pc_main.cpp
|
||||
index 9cfa47a..0a822c8 100644
|
||||
--- a/native/src/pc/runtime/pc_main.cpp
|
||||
+++ b/native/src/pc/runtime/pc_main.cpp
|
||||
@@ -42,6 +42,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
+#include <aurora/texture.hpp> // the PS5 port's button glyphs (pc_aurora_init)
|
||||
+
|
||||
#if defined(__SWITCH__)
|
||||
#include "JSystem/JAudio/JASAudioThread.h"
|
||||
#include "m_Do/m_Do_dvd_thread.h"
|
||||
@@ -232,7 +234,25 @@ void pc_aurora_init(int argc, char* argv[]) {
|
||||
config.windowWidth = (int)(480 * 3 / 2 * pc_aspect_ratio() + 0.5f);
|
||||
config.windowHeight = 480 * 3 / 2;
|
||||
config.logLevel = LOG_INFO;
|
||||
- config.allowTextureDumps = false;
|
||||
+ // COS_TEXTURE_DUMPS=1 (the PS5 port's addition, for every host): Aurora writes each texture it
|
||||
+ // draws into the cache folder's texture_dumps, named as a replacement for it must be named.
|
||||
+ {
|
||||
+ const char* dumps = getenv("COS_TEXTURE_DUMPS");
|
||||
+ config.allowTextureDumps = dumps != nullptr && strcmp(dumps, "1") == 0;
|
||||
+ }
|
||||
+#if defined(__PROSPERO__)
|
||||
+ // PlayStation 5: a file named texture-dumps in the title's folder turns them on too ...
|
||||
+ config.allowTextureDumps = config.allowTextureDumps || access("/app0/texture-dumps", F_OK) == 0;
|
||||
+ if (config.allowTextureDumps) {
|
||||
+ // ... and FTP may read them: the title's folders are its owner's alone otherwise
|
||||
+ char dumps[PATH_MAX + 32];
|
||||
+ snprintf(dumps, sizeof(dumps), "%s/texture_dumps", sCachePath);
|
||||
+ for (const char* dir : {(const char*)sUserPath, (const char*)sCachePath, (const char*)dumps}) {
|
||||
+ mkdir(dir, 0777);
|
||||
+ chmod(dir, 0777);
|
||||
+ }
|
||||
+ }
|
||||
+#endif
|
||||
config.mem1Size = kMem1Size;
|
||||
config.mem2Size = kMem2Size;
|
||||
config.blockingPipelines = gConfig.syncPipelines;
|
||||
@@ -249,6 +269,25 @@ void pc_aurora_init(int argc, char* argv[]) {
|
||||
}
|
||||
});
|
||||
#endif
|
||||
+ // The PS5 port's button prompts: the DualSense's symbols in place of the game's letters,
|
||||
+ // pictures the port ships in the title's folder (res/ps5-glyphs, named as Aurora names a
|
||||
+ // replacement: tex1_<w>x<h>_<hash>_<format>.png). COS_BUTTON_GLYPHS names another folder, on
|
||||
+ // any host (to try them without the console). An HD texture pack (pc_hd_textures.cpp) is a
|
||||
+ // group of its own.
|
||||
+ {
|
||||
+ const char* glyphs = getenv("COS_BUTTON_GLYPHS");
|
||||
+#if defined(__PROSPERO__)
|
||||
+ if (glyphs == nullptr || glyphs[0] == '\0') {
|
||||
+ glyphs = "/app0/res/ps5-glyphs";
|
||||
+ }
|
||||
+#endif
|
||||
+ if (glyphs != nullptr && glyphs[0] != '\0' && access(glyphs, F_OK) == 0) {
|
||||
+ static aurora::texture::ReplacementGroup sGlyphs;
|
||||
+ sGlyphs = aurora::texture::load_replacement_directory(glyphs);
|
||||
+ writef(STDERR_FILENO, "[cos] button glyphs: %zu replacement(s) from %s\n", sGlyphs.registrations.size(),
|
||||
+ glyphs);
|
||||
+ }
|
||||
+ }
|
||||
applyFrameBufferScale(info.windowSize);
|
||||
if (pc_aspect_wide()) {
|
||||
// The game draws an anamorphic picture into the 640x480 EFB (pc_aspect.h): Aurora presents
|
||||
|
After Width: | Height: | Size: 476 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"ageLevel": {
|
||||
"default": 0
|
||||
},
|
||||
"applicationCategoryType": 0,
|
||||
"applicationDrmType": "free",
|
||||
"attribute": 0,
|
||||
"attribute2": 0,
|
||||
"attribute3": 524352,
|
||||
"conceptId": "99309",
|
||||
"contentBadgeType": 1,
|
||||
"contentId": "UP9000-PPSA99309_00-WINDWAKER0000000",
|
||||
"contentVersion": "01.000.000",
|
||||
"downloadDataSize": 256,
|
||||
"gameIntent": {
|
||||
"permittedIntents": [
|
||||
{
|
||||
"intentType": "launchActivity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"kernel": {
|
||||
"flexibleMemorySize": 1073741824
|
||||
},
|
||||
"localizedParameters": {
|
||||
"defaultLanguage": "en-US",
|
||||
"en-US": {
|
||||
"titleName": "Wind Waker"
|
||||
}
|
||||
},
|
||||
"masterVersion": "01.00",
|
||||
"pubtools": {
|
||||
"creationDate": "2026-10-12 00:00:00",
|
||||
"loudnessSnd0": "-28.00",
|
||||
"toolVersion": "2.00"
|
||||
},
|
||||
"requiredSystemSoftwareVersion": "0x0000000000000000",
|
||||
"sdkVersion": "0x0000000000000000",
|
||||
"titleId": "PPSA99309",
|
||||
"versionFileUri": ""
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Wind Waker for the PlayStation 5 - SDL3's audio driver for the console.
|
||||
*
|
||||
* One playback device: an AudioOut main port, 48 kHz stereo 16-bit, a grain
|
||||
* of 256 frames. sceAudioOutOutput blocks while the console plays the grain
|
||||
* before, which paces SDL's audio thread.
|
||||
*
|
||||
* SPDX-License-Identifier: Zlib
|
||||
*/
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#ifdef SDL_AUDIO_DRIVER_PS5
|
||||
|
||||
#include "audio/SDL_audiodev_c.h"
|
||||
#include "audio/SDL_sysaudio.h"
|
||||
|
||||
int sceAudioOutInit(void);
|
||||
int sceAudioOutOpen(int32_t user, int32_t type, int32_t index, uint32_t grain, uint32_t rate, uint32_t format);
|
||||
int sceAudioOutOutput(int32_t port, const void *samples);
|
||||
int sceAudioOutClose(int32_t port);
|
||||
|
||||
#define PS5_AUDIO_GRAIN 256
|
||||
#define PS5_AUDIO_ALREADY_INITIALISED ((int)0x8026000e)
|
||||
#define PS5_AUDIO_USER_SYSTEM 0xff
|
||||
#define PS5_AUDIO_PORT_MAIN 0
|
||||
#define PS5_AUDIO_FORMAT_S16_STEREO 1
|
||||
|
||||
struct SDL_PrivateAudioData
|
||||
{
|
||||
int port;
|
||||
Uint8 *buffer;
|
||||
};
|
||||
|
||||
static bool PS5AUDIO_OpenDevice(SDL_AudioDevice *device)
|
||||
{
|
||||
int init;
|
||||
|
||||
device->hidden = (struct SDL_PrivateAudioData *)SDL_calloc(1, sizeof(*device->hidden));
|
||||
if (!device->hidden) {
|
||||
return false;
|
||||
}
|
||||
device->hidden->port = -1;
|
||||
|
||||
device->spec.format = SDL_AUDIO_S16LE;
|
||||
device->spec.channels = 2;
|
||||
device->spec.freq = 48000;
|
||||
device->sample_frames = PS5_AUDIO_GRAIN;
|
||||
SDL_UpdatedAudioDeviceFormat(device);
|
||||
|
||||
device->hidden->buffer = (Uint8 *)SDL_aligned_alloc(64, device->buffer_size);
|
||||
if (!device->hidden->buffer) {
|
||||
return false;
|
||||
}
|
||||
SDL_memset(device->hidden->buffer, 0, device->buffer_size);
|
||||
|
||||
init = sceAudioOutInit();
|
||||
if (init < 0 && init != PS5_AUDIO_ALREADY_INITIALISED) {
|
||||
return SDL_SetError("sceAudioOutInit: 0x%08x", (unsigned)init);
|
||||
}
|
||||
device->hidden->port = sceAudioOutOpen(PS5_AUDIO_USER_SYSTEM, PS5_AUDIO_PORT_MAIN, 0, PS5_AUDIO_GRAIN, 48000,
|
||||
PS5_AUDIO_FORMAT_S16_STEREO);
|
||||
if (device->hidden->port < 0) {
|
||||
return SDL_SetError("sceAudioOutOpen: 0x%08x", (unsigned)device->hidden->port);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PS5AUDIO_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size)
|
||||
{
|
||||
return sceAudioOutOutput(device->hidden->port, buffer) >= 0;
|
||||
}
|
||||
|
||||
static bool PS5AUDIO_WaitDevice(SDL_AudioDevice *device)
|
||||
{
|
||||
return true; // PlayDevice blocks
|
||||
}
|
||||
|
||||
static Uint8 *PS5AUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size)
|
||||
{
|
||||
return device->hidden->buffer;
|
||||
}
|
||||
|
||||
static void PS5AUDIO_CloseDevice(SDL_AudioDevice *device)
|
||||
{
|
||||
if (device->hidden) {
|
||||
if (device->hidden->port >= 0) {
|
||||
(void)sceAudioOutOutput(device->hidden->port, NULL); // drain
|
||||
(void)sceAudioOutClose(device->hidden->port);
|
||||
}
|
||||
SDL_aligned_free(device->hidden->buffer);
|
||||
SDL_free(device->hidden);
|
||||
device->hidden = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool PS5AUDIO_Init(SDL_AudioDriverImpl *impl)
|
||||
{
|
||||
impl->OpenDevice = PS5AUDIO_OpenDevice;
|
||||
impl->PlayDevice = PS5AUDIO_PlayDevice;
|
||||
impl->WaitDevice = PS5AUDIO_WaitDevice;
|
||||
impl->GetDeviceBuf = PS5AUDIO_GetDeviceBuf;
|
||||
impl->CloseDevice = PS5AUDIO_CloseDevice;
|
||||
impl->OnlyHasDefaultPlaybackDevice = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
AudioBootStrap PS5AUDIO_bootstrap = {
|
||||
"ps5", "PlayStation 5 audio driver", PS5AUDIO_Init, false, false
|
||||
};
|
||||
|
||||
#endif // SDL_AUDIO_DRIVER_PS5
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Wind Waker for the PlayStation 5 - SDL3's joystick driver for the console.
|
||||
*
|
||||
* Up to four controllers, one per signed-in user, read through the title's
|
||||
* platform layer (ps5/src/platform.h). Each is a gamepad with a mapping of its
|
||||
* own, and reports itself as a DualSense so a title shows the right glyphs.
|
||||
*
|
||||
* SPDX-License-Identifier: Zlib
|
||||
*/
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#ifdef SDL_JOYSTICK_PS5
|
||||
|
||||
#include "joystick/SDL_sysjoystick.h"
|
||||
#include "joystick/SDL_joystick_c.h"
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
/* Joystick buttons, in this order */
|
||||
static const uint32_t ps5_buttons[] = {
|
||||
PAD_CROSS, PAD_CIRCLE, PAD_SQUARE, PAD_TRIANGLE,
|
||||
PAD_L1, PAD_R1, PAD_L3, PAD_R3,
|
||||
PAD_OPTIONS, PAD_TOUCH_PAD,
|
||||
PAD_UP, PAD_DOWN, PAD_LEFT, PAD_RIGHT,
|
||||
};
|
||||
#define PS5_BUTTONS ((int)SDL_arraysize(ps5_buttons))
|
||||
#define PS5_AXES 6 // left x, left y, right x, right y, L2, R2
|
||||
|
||||
static bool ps5_pad_opened;
|
||||
static uint32_t ps5_connected; // a bit a player, as SDL knows them
|
||||
static int ps5_device_player[PAD_PLAYERS]; // device index -> player
|
||||
static int ps5_devices;
|
||||
|
||||
static SDL_JoystickID PS5_InstanceID(int player)
|
||||
{
|
||||
return (SDL_JoystickID)(player + 1);
|
||||
}
|
||||
|
||||
static void PS5_ListDevices(void)
|
||||
{
|
||||
ps5_devices = 0;
|
||||
for (int player = 0; player < PAD_PLAYERS; player++) {
|
||||
if (ps5_connected & (1u << player)) {
|
||||
ps5_device_player[ps5_devices++] = player;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void PS5_JoystickDetect(void)
|
||||
{
|
||||
struct pad pad;
|
||||
uint32_t now;
|
||||
|
||||
if (!ps5_pad_opened) {
|
||||
return;
|
||||
}
|
||||
pad_poll(&pad);
|
||||
now = pad_players();
|
||||
for (int player = 0; player < PAD_PLAYERS; player++) {
|
||||
const uint32_t bit = 1u << player;
|
||||
if ((now & bit) && !(ps5_connected & bit)) {
|
||||
ps5_connected |= bit;
|
||||
PS5_ListDevices();
|
||||
SDL_PrivateJoystickAdded(PS5_InstanceID(player));
|
||||
} else if (!(now & bit) && (ps5_connected & bit)) {
|
||||
ps5_connected &= ~bit;
|
||||
PS5_ListDevices();
|
||||
SDL_PrivateJoystickRemoved(PS5_InstanceID(player));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool PS5_JoystickInit(void)
|
||||
{
|
||||
ps5_pad_opened = pad_open();
|
||||
PS5_JoystickDetect();
|
||||
return true;
|
||||
}
|
||||
|
||||
static int PS5_JoystickGetCount(void)
|
||||
{
|
||||
return ps5_devices;
|
||||
}
|
||||
|
||||
static bool PS5_JoystickIsDevicePresent(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char *PS5_JoystickGetDeviceName(int device_index)
|
||||
{
|
||||
return "DualSense Wireless Controller";
|
||||
}
|
||||
|
||||
static const char *PS5_JoystickGetDevicePath(int device_index)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int PS5_JoystickGetDeviceSteamVirtualGamepadSlot(int device_index)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int PS5_JoystickGetDevicePlayerIndex(int device_index)
|
||||
{
|
||||
return ps5_device_player[device_index];
|
||||
}
|
||||
|
||||
static void PS5_JoystickSetDevicePlayerIndex(int device_index, int player_index)
|
||||
{
|
||||
}
|
||||
|
||||
static SDL_GUID PS5_JoystickGetDeviceGUID(int device_index)
|
||||
{
|
||||
/* Sony's DualSense, with a driver signature no mapping database entry has:
|
||||
* the mapping is this driver's */
|
||||
return SDL_CreateJoystickGUID(SDL_HARDWARE_BUS_BLUETOOTH, 0x054c, 0x0ce6, 0, NULL,
|
||||
PS5_JoystickGetDeviceName(device_index), 'p', 0);
|
||||
}
|
||||
|
||||
static SDL_JoystickID PS5_JoystickGetDeviceInstanceID(int device_index)
|
||||
{
|
||||
return PS5_InstanceID(ps5_device_player[device_index]);
|
||||
}
|
||||
|
||||
static bool PS5_JoystickOpen(SDL_Joystick *joystick, int device_index)
|
||||
{
|
||||
joystick->nbuttons = PS5_BUTTONS;
|
||||
joystick->naxes = PS5_AXES;
|
||||
joystick->nhats = 0;
|
||||
SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN, true);
|
||||
SDL_SetBooleanProperty(SDL_GetJoystickProperties(joystick), SDL_PROP_JOYSTICK_CAP_RGB_LED_BOOLEAN, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PS5_JoystickRumble(SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
|
||||
{
|
||||
pad_player_vibrate((int)joystick->instance_id - 1, low_frequency_rumble / 65535.0f, high_frequency_rumble / 65535.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PS5_JoystickRumbleTriggers(SDL_Joystick *joystick, Uint16 left, Uint16 right)
|
||||
{
|
||||
return SDL_Unsupported();
|
||||
}
|
||||
|
||||
static bool PS5_JoystickSetLED(SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
|
||||
{
|
||||
pad_player_light_bar((int)joystick->instance_id - 1, red, green, blue);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PS5_JoystickSendEffect(SDL_Joystick *joystick, const void *data, int size)
|
||||
{
|
||||
return SDL_Unsupported();
|
||||
}
|
||||
|
||||
static bool PS5_JoystickSetSensorsEnabled(SDL_Joystick *joystick, bool enabled)
|
||||
{
|
||||
return SDL_Unsupported();
|
||||
}
|
||||
|
||||
static Sint16 PS5_Stick(float value)
|
||||
{
|
||||
value = SDL_clamp(value, -1.0f, 1.0f);
|
||||
return (Sint16)(value * 32767.0f);
|
||||
}
|
||||
|
||||
static Sint16 PS5_Trigger(float value)
|
||||
{
|
||||
value = SDL_clamp(value, 0.0f, 1.0f);
|
||||
return (Sint16)(value * 65535.0f - 32768.0f);
|
||||
}
|
||||
|
||||
static void PS5_JoystickUpdate(SDL_Joystick *joystick)
|
||||
{
|
||||
const Uint64 timestamp = SDL_GetTicksNS();
|
||||
struct pad pad;
|
||||
|
||||
/* PS5_JoystickDetect polled the pads just before */
|
||||
if (!pad_player((int)joystick->instance_id - 1, &pad) || (pad.held & PAD_INTERCEPTED)) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < PS5_BUTTONS; i++) {
|
||||
SDL_SendJoystickButton(timestamp, joystick, (Uint8)i, (pad.held & ps5_buttons[i]) != 0);
|
||||
}
|
||||
SDL_SendJoystickAxis(timestamp, joystick, 0, PS5_Stick(pad.left_x));
|
||||
SDL_SendJoystickAxis(timestamp, joystick, 1, PS5_Stick(pad.left_y));
|
||||
SDL_SendJoystickAxis(timestamp, joystick, 2, PS5_Stick(pad.right_x));
|
||||
SDL_SendJoystickAxis(timestamp, joystick, 3, PS5_Stick(pad.right_y));
|
||||
SDL_SendJoystickAxis(timestamp, joystick, 4, PS5_Trigger(pad.l2));
|
||||
SDL_SendJoystickAxis(timestamp, joystick, 5, PS5_Trigger(pad.r2));
|
||||
}
|
||||
|
||||
static void PS5_JoystickClose(SDL_Joystick *joystick)
|
||||
{
|
||||
pad_player_vibrate((int)joystick->instance_id - 1, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
static void PS5_JoystickQuit(void)
|
||||
{
|
||||
}
|
||||
|
||||
static bool PS5_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
|
||||
{
|
||||
*out = (SDL_GamepadMapping){
|
||||
.a = { EMappingKind_Button, 0 },
|
||||
.b = { EMappingKind_Button, 1 },
|
||||
.x = { EMappingKind_Button, 2 },
|
||||
.y = { EMappingKind_Button, 3 },
|
||||
.leftshoulder = { EMappingKind_Button, 4 },
|
||||
.rightshoulder = { EMappingKind_Button, 5 },
|
||||
.leftstick = { EMappingKind_Button, 6 },
|
||||
.rightstick = { EMappingKind_Button, 7 },
|
||||
.start = { EMappingKind_Button, 8 },
|
||||
/* the pad's CREATE button is the shell's: the touch pad's click is "back" */
|
||||
.back = { EMappingKind_Button, 9 },
|
||||
.dpup = { EMappingKind_Button, 10 },
|
||||
.dpdown = { EMappingKind_Button, 11 },
|
||||
.dpleft = { EMappingKind_Button, 12 },
|
||||
.dpright = { EMappingKind_Button, 13 },
|
||||
.leftx = { EMappingKind_Axis, 0 },
|
||||
.lefty = { EMappingKind_Axis, 1 },
|
||||
.rightx = { EMappingKind_Axis, 2 },
|
||||
.righty = { EMappingKind_Axis, 3 },
|
||||
.lefttrigger = { EMappingKind_Axis, 4 },
|
||||
.righttrigger = { EMappingKind_Axis, 5 },
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
SDL_JoystickDriver SDL_PS5_JoystickDriver = {
|
||||
PS5_JoystickInit,
|
||||
PS5_JoystickGetCount,
|
||||
PS5_JoystickDetect,
|
||||
PS5_JoystickIsDevicePresent,
|
||||
PS5_JoystickGetDeviceName,
|
||||
PS5_JoystickGetDevicePath,
|
||||
PS5_JoystickGetDeviceSteamVirtualGamepadSlot,
|
||||
PS5_JoystickGetDevicePlayerIndex,
|
||||
PS5_JoystickSetDevicePlayerIndex,
|
||||
PS5_JoystickGetDeviceGUID,
|
||||
PS5_JoystickGetDeviceInstanceID,
|
||||
PS5_JoystickOpen,
|
||||
PS5_JoystickRumble,
|
||||
PS5_JoystickRumbleTriggers,
|
||||
PS5_JoystickSetLED,
|
||||
PS5_JoystickSendEffect,
|
||||
PS5_JoystickSetSensorsEnabled,
|
||||
PS5_JoystickUpdate,
|
||||
PS5_JoystickClose,
|
||||
PS5_JoystickQuit,
|
||||
PS5_JoystickGetGamepadMapping,
|
||||
};
|
||||
|
||||
#endif // SDL_JOYSTICK_PS5
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Wind Waker for the PlayStation 5 - SDL3's video driver for the console.
|
||||
*
|
||||
* The console has one output and no windows: this gives SDL one display and
|
||||
* lets it keep its window bookkeeping (size, flags, events) for a window that
|
||||
* is the whole screen. Nothing is drawn through SDL; the title's renderer
|
||||
* presents through VK_KHR_display on its own.
|
||||
*
|
||||
* The size is the options menu's "Output resolution" (COS_PS5_OUTPUT in the
|
||||
* game's settings file, the picture's height; 3840x2160 unless it says
|
||||
* otherwise): Aurora sizes its surface and the game's "window size" by it.
|
||||
* SDL_PS5_DISPLAY=WIDTHxHEIGHT in the environment sets another.
|
||||
*
|
||||
* SPDX-License-Identifier: Zlib
|
||||
*/
|
||||
#include "SDL_internal.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef SDL_VIDEO_DRIVER_PS5
|
||||
|
||||
#include "video/SDL_sysvideo.h"
|
||||
#include "events/SDL_events_c.h"
|
||||
|
||||
static bool PS5_VideoInit(SDL_VideoDevice *_this)
|
||||
{
|
||||
SDL_DisplayMode mode;
|
||||
int w = 3840, h = 2160;
|
||||
const char *size = SDL_getenv("SDL_PS5_DISPLAY");
|
||||
FILE *settings = size ? NULL : fopen("/app0/user/settings.ini", "r");
|
||||
|
||||
if (settings) {
|
||||
char line[256];
|
||||
int height;
|
||||
|
||||
while (fgets(line, sizeof(line), settings) != NULL) {
|
||||
if (SDL_sscanf(line, "COS_PS5_OUTPUT=%d", &height) == 1 && (height == 1080 || height == 1440)) {
|
||||
w = height == 1080 ? 1920 : 2560;
|
||||
h = height;
|
||||
}
|
||||
}
|
||||
fclose(settings);
|
||||
}
|
||||
if (size && SDL_sscanf(size, "%dx%d", &w, &h) != 2) {
|
||||
w = 3840;
|
||||
h = 2160;
|
||||
}
|
||||
SDL_zero(mode);
|
||||
mode.format = SDL_PIXELFORMAT_XRGB8888;
|
||||
mode.w = w;
|
||||
mode.h = h;
|
||||
mode.refresh_rate = 60.0f;
|
||||
return SDL_AddBasicVideoDisplay(&mode) != 0;
|
||||
}
|
||||
|
||||
static void PS5_VideoQuit(SDL_VideoDevice *_this)
|
||||
{
|
||||
}
|
||||
|
||||
static void PS5_PumpEvents(SDL_VideoDevice *_this)
|
||||
{
|
||||
}
|
||||
|
||||
static bool PS5_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
|
||||
{
|
||||
/* The window is the screen, and always has the pad's focus */
|
||||
SDL_SetKeyboardFocus(window);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PS5_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static void PS5_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window)
|
||||
{
|
||||
/* The size stays the display's */
|
||||
}
|
||||
|
||||
static SDL_FullscreenResult PS5_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen)
|
||||
{
|
||||
return SDL_FULLSCREEN_SUCCEEDED;
|
||||
}
|
||||
|
||||
static void PS5_DeleteDevice(SDL_VideoDevice *device)
|
||||
{
|
||||
SDL_free(device);
|
||||
}
|
||||
|
||||
static SDL_VideoDevice *PS5_CreateDevice(void)
|
||||
{
|
||||
SDL_VideoDevice *device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice));
|
||||
if (!device) {
|
||||
return NULL;
|
||||
}
|
||||
device->VideoInit = PS5_VideoInit;
|
||||
device->VideoQuit = PS5_VideoQuit;
|
||||
device->PumpEvents = PS5_PumpEvents;
|
||||
device->CreateSDLWindow = PS5_CreateWindow;
|
||||
device->SetWindowPosition = PS5_SetWindowPosition;
|
||||
device->SetWindowSize = PS5_SetWindowSize;
|
||||
device->SetWindowFullscreen = PS5_SetWindowFullscreen;
|
||||
device->free = PS5_DeleteDevice;
|
||||
device->device_caps = VIDEO_DEVICE_CAPS_FULLSCREEN_ONLY;
|
||||
return device;
|
||||
}
|
||||
|
||||
VideoBootStrap PS5_bootstrap = {
|
||||
"ps5", "PlayStation 5 video driver",
|
||||
PS5_CreateDevice,
|
||||
NULL, // no ShowMessageBox implementation
|
||||
false
|
||||
};
|
||||
|
||||
#endif // SDL_VIDEO_DRIVER_PS5
|
||||
@@ -0,0 +1,438 @@
|
||||
/*
|
||||
* PS5 Vulkan Template - the console: klog, the splash, the pad, sound, time, the exit.
|
||||
*
|
||||
* Copyright (C) 2026 Mihawk
|
||||
*
|
||||
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
|
||||
*
|
||||
* Every console call here is one PS5_vkQuake, PS5_RetroArch or PS5_Vulkan's
|
||||
* smoke test runs with; no SDK header declares them.
|
||||
*/
|
||||
#include "platform.h"
|
||||
|
||||
#include <ps5platform/klog.h>
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
int sceSystemServiceHideSplashScreen(void);
|
||||
int sceSystemServiceLoadExec(const char *path, const char *const *argv);
|
||||
int sceUserServiceInitialize(const void *params);
|
||||
int sceUserServiceGetInitialUser(int32_t *user_id);
|
||||
/* The signed-in users, at most four; unused entries are -1. */
|
||||
struct user_list {
|
||||
int32_t user_id[4];
|
||||
};
|
||||
int sceUserServiceGetLoginUserIdList(struct user_list *list);
|
||||
int scePadInit(void);
|
||||
int scePadOpen(int32_t user_id, int32_t port_type, int32_t index, const void *params);
|
||||
int scePadGetHandle(int32_t user_id, int32_t port_type, int32_t index);
|
||||
int scePadRead(int32_t handle, void *samples, int32_t capacity);
|
||||
int scePadClose(int32_t handle);
|
||||
int scePadSetVibrationMode(int32_t handle, int32_t mode);
|
||||
int scePadSetVibration(int32_t handle, const void *vibration);
|
||||
int scePadSetLightBar(int32_t handle, const void *color);
|
||||
int sceAudioOutInit(void);
|
||||
int sceAudioOutOpen(int32_t user, int32_t type, int32_t index, uint32_t grain, uint32_t rate,
|
||||
uint32_t format);
|
||||
int sceAudioOutOutput(int32_t port, const void *samples);
|
||||
int sceAudioOutClose(int32_t port);
|
||||
int sceKernelUsleep(uint32_t microseconds);
|
||||
|
||||
void
|
||||
platform_init(const char *title_name)
|
||||
{
|
||||
static char prefix[64];
|
||||
snprintf(prefix, sizeof(prefix), "[%s] ", title_name);
|
||||
const int captured = ps5_klog_capture_stderr(prefix);
|
||||
say("starts (standard error to klog: %s)", captured == 0 ? "yes" : "no");
|
||||
/* The shell's splash covers the title's frames until it is dismissed. */
|
||||
say("splash dismissed: %d", sceSystemServiceHideSplashScreen());
|
||||
}
|
||||
|
||||
void
|
||||
say(const char *format, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vfprintf(stderr, format, args);
|
||||
va_end(args);
|
||||
fputc('\n', stderr);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
double
|
||||
now_seconds(void)
|
||||
{
|
||||
struct timespec t;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t);
|
||||
return (double)t.tv_sec + (double)t.tv_nsec / 1e9;
|
||||
}
|
||||
|
||||
/* The title ends by asking the shell to close it. exit() and returning from
|
||||
* _start both kill it, and the console then reports a crash over a run that
|
||||
* worked. The CRT calls this after main returns; the shell closes the title
|
||||
* asynchronously, so it must wait and never return. */
|
||||
void
|
||||
catchReturnFromMain(int status)
|
||||
{
|
||||
say("exit status %d: asking the shell to close the title", status);
|
||||
fflush(NULL);
|
||||
(void)sceSystemServiceLoadExec("exit", NULL);
|
||||
for (;;)
|
||||
sceKernelUsleep(100000);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- the pad */
|
||||
|
||||
/* One sample as the console writes it: 120 bytes, the layout PS5_vkQuake and
|
||||
* ProsperoLight read. */
|
||||
struct pad_sample {
|
||||
uint32_t buttons;
|
||||
uint8_t left_x, left_y, right_x, right_y, l2, r2;
|
||||
uint8_t reserved[66];
|
||||
int32_t connected;
|
||||
uint64_t timestamp_us;
|
||||
uint8_t extension[16];
|
||||
uint8_t connected_count;
|
||||
uint8_t remaining[15];
|
||||
};
|
||||
_Static_assert(sizeof(struct pad_sample) == 120, "the console's pad samples are 120 bytes");
|
||||
_Static_assert(offsetof(struct pad_sample, connected) == 0x4c, "connection state sits at 0x4c");
|
||||
_Static_assert(offsetof(struct pad_sample, timestamp_us) == 0x50, "the timestamp sits at 0x50");
|
||||
|
||||
/* The two-motor rumble, as PS5_vkQuake and ProsperoLight drive it (the other
|
||||
* mode is the DualSense's haptics). */
|
||||
#define PAD_VIBRATION_COMPATIBLE 2
|
||||
|
||||
/* A player: a signed-in user's controller, PS5_ProsperoEden's way. The console
|
||||
* pairs each controller with a user, and scePadOpen opens a user's. Player 0 is
|
||||
* the user who started the title and stays theirs; about once a second
|
||||
* pad_poll reads the signed-in users again, a new user's controller takes the
|
||||
* first free player, and a player is let go when their user signs out. */
|
||||
struct pad_player_state {
|
||||
int32_t user;
|
||||
int32_t handle;
|
||||
bool connected;
|
||||
struct pad_sample last;
|
||||
struct pad_reading taken[64];
|
||||
int taken_count;
|
||||
};
|
||||
|
||||
static struct pad_player_state pad_states[PAD_PLAYERS];
|
||||
static bool pad_service;
|
||||
static double pad_next_scan;
|
||||
/* A user whose controller would not open, said once rather than every scan. */
|
||||
static int32_t pad_unopened = -1;
|
||||
#define PAD_SCAN_SECONDS 1.0
|
||||
|
||||
/* A player's state while they have a controller, else NULL (and always NULL
|
||||
* before pad_open has set the players up). */
|
||||
static struct pad_player_state *
|
||||
pad_state(int player)
|
||||
{
|
||||
if (!pad_service || player < 0 || player >= PAD_PLAYERS || pad_states[player].handle < 0)
|
||||
return NULL;
|
||||
return &pad_states[player];
|
||||
}
|
||||
|
||||
static void
|
||||
pad_rest(struct pad_player_state *state)
|
||||
{
|
||||
memset(state, 0, sizeof(*state));
|
||||
state->user = state->handle = -1;
|
||||
state->last.left_x = state->last.left_y = state->last.right_x = state->last.right_y = 128;
|
||||
}
|
||||
|
||||
static bool
|
||||
pad_player_open(int player, int32_t user, int attempts)
|
||||
{
|
||||
struct pad_player_state *state = &pad_states[player];
|
||||
pad_rest(state);
|
||||
/* A title can start before the pad service has published the device. */
|
||||
int32_t handle = -1;
|
||||
for (int attempt = 0; attempt < attempts && handle < 0; attempt++) {
|
||||
handle = scePadOpen(user, 0, 0, NULL);
|
||||
if (handle < 0 && attempt + 1 < attempts)
|
||||
sceKernelUsleep(100000);
|
||||
}
|
||||
/* A handle this process already holds for the user is reused. */
|
||||
if (handle < 0)
|
||||
handle = scePadGetHandle(user, 0, 0);
|
||||
if (handle < 0) {
|
||||
if (user != pad_unopened)
|
||||
say("pad: scePadOpen failed for user %d (player %d)", (int)user, player);
|
||||
pad_unopened = user;
|
||||
return false;
|
||||
}
|
||||
state->user = user;
|
||||
state->handle = handle;
|
||||
say("pad: player %d opened for user %d, rumble mode %d", player, (int)user,
|
||||
scePadSetVibrationMode(handle, PAD_VIBRATION_COMPATIBLE));
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
pad_player_close(int player, const char *why)
|
||||
{
|
||||
struct pad_player_state *state = &pad_states[player];
|
||||
if (state->handle < 0)
|
||||
return;
|
||||
say("pad: player %d let go (user %d %s), close %d", player, (int)state->user, why,
|
||||
scePadClose(state->handle));
|
||||
pad_rest(state);
|
||||
}
|
||||
|
||||
/* The signed-in users again: player 0 stays; a player whose user signed out is
|
||||
* let go; a new user's controller takes the first free player. */
|
||||
static void
|
||||
pad_scan(void)
|
||||
{
|
||||
struct user_list list = {{-1, -1, -1, -1}};
|
||||
if (sceUserServiceGetLoginUserIdList(&list) < 0)
|
||||
return;
|
||||
for (int player = 1; player < PAD_PLAYERS; player++) {
|
||||
if (pad_states[player].handle < 0)
|
||||
continue;
|
||||
bool signed_in = false;
|
||||
for (int i = 0; i < 4; i++)
|
||||
signed_in |= list.user_id[i] >= 0 && list.user_id[i] == pad_states[player].user;
|
||||
if (!signed_in)
|
||||
pad_player_close(player, "signed out");
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const int32_t user = list.user_id[i];
|
||||
bool known = user < 0;
|
||||
for (int player = 0; player < PAD_PLAYERS && !known; player++)
|
||||
known = pad_states[player].handle >= 0 && pad_states[player].user == user;
|
||||
for (int player = 0; player < PAD_PLAYERS && !known; player++) {
|
||||
if (pad_states[player].handle < 0) {
|
||||
(void)pad_player_open(player, user, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
pad_open(void)
|
||||
{
|
||||
for (int player = 0; player < PAD_PLAYERS; player++)
|
||||
pad_rest(&pad_states[player]);
|
||||
(void)sceUserServiceInitialize(NULL);
|
||||
int32_t user = -1;
|
||||
if (sceUserServiceGetInitialUser(&user) < 0 || scePadInit() < 0) {
|
||||
say("pad: no user or no pad service");
|
||||
return false;
|
||||
}
|
||||
pad_service = true;
|
||||
const bool opened = pad_player_open(0, user, 10);
|
||||
pad_scan();
|
||||
pad_next_scan = now_seconds() + PAD_SCAN_SECONDS;
|
||||
return opened;
|
||||
}
|
||||
|
||||
static float
|
||||
stick(uint8_t value)
|
||||
{
|
||||
const float v = ((float)value - 128.0f) / 127.0f;
|
||||
const float dead = 0.12f;
|
||||
if (v > -dead && v < dead)
|
||||
return 0.0f;
|
||||
return v > 0 ? (v - dead) / (1.0f - dead) : (v + dead) / (1.0f - dead);
|
||||
}
|
||||
|
||||
static void
|
||||
pad_fill(const struct pad_player_state *state, struct pad *pad)
|
||||
{
|
||||
const uint32_t before = pad->held;
|
||||
pad->held = state->handle >= 0 ? state->last.buttons : 0;
|
||||
pad->pressed = pad->held & ~before;
|
||||
pad->left_x = stick(state->last.left_x);
|
||||
pad->left_y = stick(state->last.left_y);
|
||||
pad->right_x = stick(state->last.right_x);
|
||||
pad->right_y = stick(state->last.right_y);
|
||||
pad->l2 = state->last.l2 / 255.0f;
|
||||
pad->r2 = state->last.r2 / 255.0f;
|
||||
}
|
||||
|
||||
void
|
||||
pad_poll(struct pad *pad)
|
||||
{
|
||||
if (pad_service && now_seconds() >= pad_next_scan) {
|
||||
pad_scan();
|
||||
pad_next_scan = now_seconds() + PAD_SCAN_SECONDS;
|
||||
}
|
||||
static struct pad_sample samples[64];
|
||||
for (int player = 0; player < PAD_PLAYERS; player++) {
|
||||
struct pad_player_state *state = pad_state(player);
|
||||
if (state == NULL)
|
||||
continue;
|
||||
state->taken_count = 0;
|
||||
const int count = scePadRead(state->handle, samples, 64);
|
||||
if (count < 0)
|
||||
state->connected = false;
|
||||
for (int i = 0; i < count && i < 64; i++) {
|
||||
const struct pad_sample *s = &samples[i];
|
||||
state->taken[state->taken_count++] = (struct pad_reading){
|
||||
s->buttons, s->left_x, s->left_y, s->right_x, s->right_y, s->l2, s->r2,
|
||||
s->connected != 0, s->timestamp_us};
|
||||
state->connected = s->connected != 0;
|
||||
if (s->connected && !(s->buttons & PAD_INTERCEPTED))
|
||||
state->last = *s;
|
||||
}
|
||||
}
|
||||
(void)pad_player(0, pad);
|
||||
}
|
||||
|
||||
uint32_t
|
||||
pad_players(void)
|
||||
{
|
||||
uint32_t players = 0;
|
||||
for (int player = 0; player < PAD_PLAYERS; player++) {
|
||||
const struct pad_player_state *state = pad_state(player);
|
||||
if (state != NULL && state->connected)
|
||||
players |= 1u << player;
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
bool
|
||||
pad_player(int player, struct pad *pad)
|
||||
{
|
||||
const struct pad_player_state *state = pad_state(player);
|
||||
if (state == NULL) {
|
||||
struct pad_player_state rest;
|
||||
pad_rest(&rest);
|
||||
pad_fill(&rest, pad);
|
||||
return false;
|
||||
}
|
||||
pad_fill(state, pad);
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
pad_player_readings(int player, const struct pad_reading **readings)
|
||||
{
|
||||
const struct pad_player_state *state = pad_state(player);
|
||||
*readings = state != NULL ? state->taken : NULL;
|
||||
return state != NULL ? state->taken_count : 0;
|
||||
}
|
||||
|
||||
int
|
||||
pad_readings(const struct pad_reading **readings)
|
||||
{
|
||||
return pad_player_readings(0, readings);
|
||||
}
|
||||
|
||||
void
|
||||
pad_player_vibrate(int player, float large, float small)
|
||||
{
|
||||
const struct pad_player_state *state = pad_state(player);
|
||||
if (state == NULL)
|
||||
return;
|
||||
const float l = large < 0.0f ? 0.0f : large > 1.0f ? 1.0f : large;
|
||||
const float s = small < 0.0f ? 0.0f : small > 1.0f ? 1.0f : small;
|
||||
const uint8_t motors[2] = {(uint8_t)(l * 255.0f), (uint8_t)(s * 255.0f)};
|
||||
(void)scePadSetVibration(state->handle, motors);
|
||||
}
|
||||
|
||||
void
|
||||
pad_vibrate(float large, float small)
|
||||
{
|
||||
pad_player_vibrate(0, large, small);
|
||||
}
|
||||
|
||||
void
|
||||
pad_player_light_bar(int player, uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
const struct pad_player_state *state = pad_state(player);
|
||||
if (state == NULL)
|
||||
return;
|
||||
const uint8_t color[4] = {r, g, b, 0};
|
||||
(void)scePadSetLightBar(state->handle, color);
|
||||
}
|
||||
|
||||
void
|
||||
pad_light_bar(uint8_t r, uint8_t g, uint8_t b)
|
||||
{
|
||||
pad_player_light_bar(0, r, g, b);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- the sound */
|
||||
|
||||
#define AUDIO_GRAIN 256
|
||||
#define AUDIO_ALREADY_INITIALISED ((int)0x8026000e)
|
||||
|
||||
static int32_t audio_port = -1;
|
||||
static pthread_t audio_thread;
|
||||
static atomic_bool audio_running;
|
||||
static audio_fill_fn audio_fill;
|
||||
static void *audio_user;
|
||||
|
||||
static void *
|
||||
audio_main(void *unused)
|
||||
{
|
||||
(void)unused;
|
||||
static int16_t grain[AUDIO_GRAIN * 2] __attribute__((aligned(64)));
|
||||
unsigned errors = 0;
|
||||
while (atomic_load(&audio_running)) {
|
||||
audio_fill(grain, AUDIO_GRAIN, audio_user);
|
||||
/* Blocks for one grain: this is what paces the thread. An output that
|
||||
* stops taking grains is said once, not left to stall the program. */
|
||||
if (sceAudioOutOutput(audio_port, grain) < 0) {
|
||||
if (errors++ == 0)
|
||||
say("audio: sceAudioOutOutput failed");
|
||||
sceKernelUsleep(5000);
|
||||
}
|
||||
}
|
||||
(void)sceAudioOutOutput(audio_port, NULL); /* drain the grain still queued */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool
|
||||
audio_start(audio_fill_fn fill, void *user)
|
||||
{
|
||||
if (audio_port >= 0 || fill == NULL)
|
||||
return false;
|
||||
const int init = sceAudioOutInit();
|
||||
if (init < 0 && init != AUDIO_ALREADY_INITIALISED) {
|
||||
say("audio: sceAudioOutInit 0x%08x", (unsigned)init);
|
||||
return false;
|
||||
}
|
||||
/* The system user's main port: 256 frames, 48 kHz, 16-bit stereo. */
|
||||
audio_port = sceAudioOutOpen(0xff, 0, 0, AUDIO_GRAIN, 48000, 1);
|
||||
if (audio_port < 0) {
|
||||
say("audio: sceAudioOutOpen 0x%08x", (unsigned)audio_port);
|
||||
audio_port = -1;
|
||||
return false;
|
||||
}
|
||||
audio_fill = fill;
|
||||
audio_user = user;
|
||||
atomic_store(&audio_running, true);
|
||||
if (pthread_create(&audio_thread, NULL, audio_main, NULL) != 0) {
|
||||
say("audio: no thread");
|
||||
atomic_store(&audio_running, false);
|
||||
(void)sceAudioOutClose(audio_port);
|
||||
audio_port = -1;
|
||||
return false;
|
||||
}
|
||||
say("audio: 48 kHz stereo on port %d", (int)audio_port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
audio_stop(void)
|
||||
{
|
||||
if (audio_port < 0)
|
||||
return;
|
||||
atomic_store(&audio_running, false);
|
||||
pthread_join(audio_thread, NULL);
|
||||
(void)sceAudioOutClose(audio_port);
|
||||
audio_port = -1;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* PS5 Vulkan Template - the console: klog, the splash, the pad, sound, time, the exit.
|
||||
*
|
||||
* Copyright (C) 2026 Mihawk
|
||||
*
|
||||
* This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT)
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Standard error into klog (each line prefixed with the title's name), and
|
||||
* the shell's splash dismissed. Call first. */
|
||||
void platform_init(const char *title_name);
|
||||
|
||||
/* One line to klog, flushed. */
|
||||
void say(const char *format, ...) __attribute__((format(printf, 1, 2)));
|
||||
|
||||
double now_seconds(void);
|
||||
|
||||
/* The pad, in the console's button numbering. */
|
||||
enum {
|
||||
PAD_L3 = 0x000002,
|
||||
PAD_R3 = 0x000004,
|
||||
PAD_OPTIONS = 0x000008,
|
||||
PAD_UP = 0x000010,
|
||||
PAD_RIGHT = 0x000020,
|
||||
PAD_DOWN = 0x000040,
|
||||
PAD_LEFT = 0x000080,
|
||||
PAD_L1 = 0x000400,
|
||||
PAD_R1 = 0x000800,
|
||||
PAD_TRIANGLE = 0x001000,
|
||||
PAD_CIRCLE = 0x002000,
|
||||
PAD_CROSS = 0x004000,
|
||||
PAD_SQUARE = 0x008000,
|
||||
PAD_TOUCH_PAD = 0x100000,
|
||||
};
|
||||
|
||||
struct pad {
|
||||
uint32_t held; /* buttons down now */
|
||||
uint32_t pressed; /* buttons that went down since the last poll */
|
||||
float left_x, left_y, right_x, right_y; /* -1..1, a dead zone removed; y is down-positive */
|
||||
float l2, r2; /* 0..1 */
|
||||
};
|
||||
|
||||
/* Up to four players, one per signed-in user: the console pairs each controller
|
||||
* with a user. Player 0 is the user who started the title and stays theirs;
|
||||
* pad_poll reads the signed-in users again about once a second, a new user's
|
||||
* controller takes the first free player, and a player is let go when their user
|
||||
* signs out. pad_poll, pad_readings, pad_vibrate and pad_light_bar are player
|
||||
* 0's; the pad_player calls reach the others. */
|
||||
#define PAD_PLAYERS 4
|
||||
|
||||
/* Open player 0's pad (the first user's), and every other signed-in user's;
|
||||
* false (and no input for player 0) when there is none. */
|
||||
bool pad_open(void);
|
||||
/* Read every player's controller; *pad is player 0's. Call once a frame. */
|
||||
void pad_poll(struct pad *pad);
|
||||
/* The players whose controller reported itself connected, a bit each (bit 0 is
|
||||
* player 0), as of the last pad_poll. */
|
||||
uint32_t pad_players(void);
|
||||
/* A player's pad as the last pad_poll read it, `pressed` against the held
|
||||
* buttons *pad had; false, and a pad at rest, when that player has none. */
|
||||
bool pad_player(int player, struct pad *pad);
|
||||
|
||||
/* Set in a reading's buttons while the shell has the pad (the home screen, a
|
||||
* system dialog): that reading is not input for the title. */
|
||||
#define PAD_INTERCEPTED 0x80000000u
|
||||
|
||||
/* One reading as the console took it, in its numbering. */
|
||||
struct pad_reading {
|
||||
uint32_t buttons;
|
||||
uint8_t left_x, left_y, right_x, right_y; /* 0..255, 128 at rest; y is down-positive */
|
||||
uint8_t l2, r2; /* 0..255 */
|
||||
bool connected;
|
||||
uint64_t timestamp_us;
|
||||
};
|
||||
|
||||
/* Every reading the last pad_poll took, oldest first (the console keeps up to
|
||||
* 64 between polls), for input models that must see a tap shorter than a
|
||||
* frame. Returns their number; *readings stays valid until the next poll. */
|
||||
int pad_readings(const struct pad_reading **readings);
|
||||
int pad_player_readings(int player, const struct pad_reading **readings);
|
||||
|
||||
/* Rumble: the large and the small motor, 0..1 each; 0, 0 stops it. */
|
||||
void pad_vibrate(float large, float small);
|
||||
void pad_player_vibrate(int player, float large, float small);
|
||||
/* The light bar's colour. */
|
||||
void pad_light_bar(uint8_t r, uint8_t g, uint8_t b);
|
||||
void pad_player_light_bar(int player, uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
/* Sound: one 48 kHz output of interleaved stereo 16-bit samples. A thread of
|
||||
* its own calls fill for 256 frames at a time and blocks while the console
|
||||
* plays them, so fill paces itself and must never wait on the render loop.
|
||||
* false when there is no output (the host reference build has none). */
|
||||
typedef void (*audio_fill_fn)(int16_t *frames, int count, void *user);
|
||||
bool audio_start(audio_fill_fn fill, void *user);
|
||||
/* Stops the thread and closes the output; call it before what fill uses goes. */
|
||||
void audio_stop(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Wind Waker for the PlayStation 5 - libc names no module a title loads exports.
|
||||
*
|
||||
* The SDK's stubs let these link, but the import is null at run time and its
|
||||
* first call jumps to address 0. tools/link-title.sh binds each name to its
|
||||
* ps5dl_ answer here (its `shims` list must name every function below).
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <errno.h>
|
||||
|
||||
/* Nothing a title writes to is a terminal: its output goes to klog. */
|
||||
int
|
||||
ps5dl_isatty(int fd)
|
||||
{
|
||||
(void)fd;
|
||||
errno = ENOTTY;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* One user, and no privilege to drop (SDL asks whether the process is setuid). */
|
||||
int
|
||||
ps5dl_getresuid(unsigned *real, unsigned *effective, unsigned *saved)
|
||||
{
|
||||
*real = *effective = *saved = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
ps5dl_getresgid(unsigned *real, unsigned *effective, unsigned *saved)
|
||||
{
|
||||
*real = *effective = *saved = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* No list of loaded modules to walk (Borealis's crash report names the module
|
||||
* of each frame with it): it reports addresses, which run-title.py symbolises. */
|
||||
int
|
||||
ps5dl_dl_iterate_phdr(int (*callback)(void *, unsigned long, void *), void *data)
|
||||
{
|
||||
(void)callback;
|
||||
(void)data;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Files belong to no one in particular. */
|
||||
int
|
||||
ps5dl_fchown(int fd, unsigned owner, unsigned group)
|
||||
{
|
||||
(void)fd;
|
||||
(void)owner;
|
||||
(void)group;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A title is one process: nothing can be started from it (SDL's process API,
|
||||
* Borealis's "restart" and "open a URL"). */
|
||||
int
|
||||
ps5dl_fork(void)
|
||||
{
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
ps5dl_vfork(void)
|
||||
{
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int
|
||||
ps5dl_setsid(void)
|
||||
{
|
||||
errno = ENOSYS;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* FreeBSD's _PC_NAME_MAX and _PC_PATH_MAX; no other limit is known. */
|
||||
long
|
||||
ps5dl_pathconf(const char *path, int name)
|
||||
{
|
||||
(void)path;
|
||||
switch (name) {
|
||||
case 4:
|
||||
return 255;
|
||||
case 5:
|
||||
return 1024;
|
||||
default:
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* The network is not used: addresses have no names. */
|
||||
int
|
||||
ps5dl_getnameinfo(const void *address, unsigned length, char *host, unsigned host_size, char *service,
|
||||
unsigned service_size, int flags)
|
||||
{
|
||||
(void)address;
|
||||
(void)length;
|
||||
(void)host;
|
||||
(void)host_size;
|
||||
(void)service;
|
||||
(void)service_size;
|
||||
(void)flags;
|
||||
return 4; /* EAI_FAIL */
|
||||
}
|
||||
|
||||
const char *
|
||||
ps5dl_gai_strerror(int code)
|
||||
{
|
||||
(void)code;
|
||||
return "name resolution is not available";
|
||||
}
|
||||
|
||||
/* lstat does not work for a title (anything but a missing file's ENOENT made
|
||||
* std::filesystem::symlink_status report an occupied path, so Aurora refused to
|
||||
* create the memory card's save, and SQLite refused to open its databases). A
|
||||
* title's folders hold no symbolic links: stat answers the same. */
|
||||
struct stat;
|
||||
int stat(const char *path, struct stat *out);
|
||||
|
||||
int
|
||||
ps5dl_lstat(const char *path, struct stat *out)
|
||||
{
|
||||
return stat(path, out);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Wind Waker for the PlayStation 5 - what runs before the game's main.
|
||||
*
|
||||
* The game's main is Aurora's and SwitchWaker's, so the console's start-up is
|
||||
* a constructor: standard error into klog and the
|
||||
* shell's splash dismissed, and the folder SDL derives its paths from.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#include "platform.h"
|
||||
|
||||
/* The shader caches the title ships (res/shader-cache, filled on a console by a
|
||||
* first start: ps5/tools/link-title.sh), for a console that has none yet: Dawn's
|
||||
* cache, where Aurora opens it, and the driver's folder. Without them a first
|
||||
* start compiles every render pipeline, which takes minutes. Copied once: what
|
||||
* is there, even half of it, is the console's own from then on. */
|
||||
static void
|
||||
seed_file(const char *from, const char *to)
|
||||
{
|
||||
char buffer[65536];
|
||||
size_t count;
|
||||
FILE *in = fopen(from, "rb");
|
||||
FILE *out = in != NULL ? fopen(to, "wb") : NULL;
|
||||
|
||||
while (out != NULL && (count = fread(buffer, 1, sizeof(buffer), in)) > 0)
|
||||
fwrite(buffer, 1, count, out);
|
||||
if (out != NULL)
|
||||
fclose(out);
|
||||
if (in != NULL)
|
||||
fclose(in);
|
||||
}
|
||||
|
||||
static void
|
||||
seed_folder(const char *from, const char *to)
|
||||
{
|
||||
char source[512], target[512];
|
||||
struct dirent *entry;
|
||||
struct stat status;
|
||||
DIR *folder = opendir(from);
|
||||
|
||||
if (folder == NULL)
|
||||
return;
|
||||
mkdir(to, 0777);
|
||||
while ((entry = readdir(folder)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
snprintf(source, sizeof(source), "%s/%s", from, entry->d_name);
|
||||
snprintf(target, sizeof(target), "%s/%s", to, entry->d_name);
|
||||
if (stat(source, &status) != 0)
|
||||
continue;
|
||||
if (S_ISDIR(status.st_mode))
|
||||
seed_folder(source, target);
|
||||
else
|
||||
seed_file(source, target);
|
||||
}
|
||||
closedir(folder);
|
||||
}
|
||||
|
||||
static void
|
||||
seed_shader_caches(void)
|
||||
{
|
||||
struct stat status;
|
||||
|
||||
if (stat("/app0/res/shader-cache/dawn_cache.db", &status) != 0)
|
||||
return;
|
||||
if (stat("/app0/user/cache/dawn_cache.db", &status) != 0) {
|
||||
mkdir("/app0/user", 0777);
|
||||
mkdir("/app0/user/cache", 0777);
|
||||
seed_file("/app0/res/shader-cache/dawn_cache.db", "/app0/user/cache/dawn_cache.db");
|
||||
fprintf(stderr, "shader cache: Dawn's copied from the title\n");
|
||||
}
|
||||
if (stat("/app0/radv-shader-cache/mesa_shader_cache_db", &status) != 0) {
|
||||
seed_folder("/app0/res/shader-cache/radv-shader-cache", "/app0/radv-shader-cache");
|
||||
fprintf(stderr, "shader cache: the driver's copied from the title\n");
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void
|
||||
ps5_startup(void)
|
||||
{
|
||||
platform_init(PS5_TITLE_NAME);
|
||||
seed_shader_caches();
|
||||
/* SDL_GetPrefPath is $XDG_DATA_HOME/<org>/<app>/: saves and settings. A title
|
||||
* sees its own folder (/app0, which is /data/homebrew/<TITLE_ID>) and nothing
|
||||
* else of /data, so they live there. Set in SDL's own copy of the environment,
|
||||
* which is all SDL_getenv reads: the console's libc keeps what setenv sets
|
||||
* apart from the `environ` SDL copies. */
|
||||
SDL_SetEnvironmentVariable(SDL_GetEnvironment(), "XDG_DATA_HOME", "/app0/userdata", true);
|
||||
SDL_SetEnvironmentVariable(SDL_GetEnvironment(), "HOME", "/app0/userdata", true);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - build the game into dist/<TITLE_ID>/.
|
||||
#
|
||||
# ps5/tools/build.sh [ninja arguments]
|
||||
#
|
||||
# Fetches and patches the sources (tools/setup-source.sh), then configures
|
||||
# SwitchWaker's own build (vendor/switchwaker/native) with the payload SDK's
|
||||
# CMake toolchain (once) and ps5/cmake/ps5-game.cmake, which gives it Dawn, SDL3
|
||||
# and nod for the console and the title's link (tools/link-title.sh). The build
|
||||
# reads no disc: COS_RUNTIME_ASSETS, with the stub headers of .deps/assets.
|
||||
#
|
||||
# PS5_BUILD_DIR the build tree (default build/game; the Linux VM's own disk
|
||||
# is several times faster than the shared folder)
|
||||
# PS5_VULKAN_DIR the PS5_Vulkan checkout (default ../PS5_Vulkan)
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
|
||||
ps5=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
root=$(dirname "$ps5")
|
||||
vulkan=$(cd -- "${PS5_VULKAN_DIR:-$root/../PS5_Vulkan}" && pwd)
|
||||
bash "$ps5/tools/setup-source.sh"
|
||||
dawn=$(bash "$ps5/tools/setup-dawn.sh")
|
||||
sdl=$(bash "$ps5/tools/setup-sdl.sh")
|
||||
sdk=$(bash "$ps5/tools/setup-sdk.sh")
|
||||
build=${PS5_BUILD_DIR:-$root/build/game}
|
||||
ninja=$(command -v ninja || echo "$HOME/.local/bin/ninja")
|
||||
export PS5_CLANG=${PS5_CLANG:-$(command -v clang || true)}
|
||||
|
||||
if [[ ! -f $build/build.ninja ]]; then
|
||||
mkdir -p "$build"
|
||||
cmake -S "$root/vendor/switchwaker/native" -B "$build" -G Ninja -DCMAKE_MAKE_PROGRAM="$ninja" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$sdk/toolchain/prospero.cmake" \
|
||||
-DCMAKE_PROJECT_cos_native_INCLUDE="$ps5/cmake/ps5-game.cmake" \
|
||||
-DPS5_PORT_DIR="$root" -DPS5_VULKAN_DIR="$vulkan" -DDAWN_DIR="$dawn" -DSDL_DIR="$sdl" \
|
||||
-DFETCHCONTENT_SOURCE_DIR_AURORA="$root/.deps/aurora" -DCOS_AURORA_PREPATCHED=ON \
|
||||
-DCOS_RECOMPCORE_DIR="$root/.deps/recompcore" \
|
||||
-DCOS_RUNTIME_ASSETS=ON -DCOS_ASSETS_DIR="$root/.deps/assets/GZLE01" \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_VERBOSE_MAKEFILE=OFF > "$build/configure.log" 2>&1 \
|
||||
|| { tail -60 "$build/configure.log" >&2; exit 1; }
|
||||
fi
|
||||
"$ninja" -C "$build" "${@:-switchwaker}"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
# Wind Waker for the PlayStation 5 - compile with an init array (a compiler launcher).
|
||||
#
|
||||
# cc-init-array.sh COMPILER ARGUMENT...
|
||||
#
|
||||
# Clang's PS5 target writes constructors to .ctors, last to first, for a
|
||||
# start-up that walks them backwards, and its driver refuses -fuse-init-array.
|
||||
# PS5_Vulkan's CRT walks the merged array forwards, so a unit's initialisers
|
||||
# ran in reverse: Dusklight's inline tables (one initialiser each, the later
|
||||
# built from the earlier) read a std::map that was not constructed yet.
|
||||
#
|
||||
# The switch is the driver's alone: it passes -fno-use-init-array to the
|
||||
# compiler proper. This asks the driver for that command (-###), drops the
|
||||
# flag and runs it, which gives .init_array in the order the source has.
|
||||
# Anything that is not one compile (-c) runs unchanged.
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
case " $* " in
|
||||
*" -c "*) ;;
|
||||
*) exec "$@" ;;
|
||||
esac
|
||||
jobs=$("$@" '-###' 2>&1 | grep -- '^ *"') || exec "$@"
|
||||
# one job, the compiler proper: a .S file is two (the preprocessor, then the assembler)
|
||||
[ "$(printf '%s\n' "$jobs" | wc -l)" -eq 1 ] || exec "$@"
|
||||
job=$(printf '%s\n' "$jobs" | grep -- ' "-cc1" ') || exec "$@"
|
||||
case $job in
|
||||
*'$'* | *'`'*) exec "$@" ;; # nothing the shell would expand
|
||||
esac
|
||||
eval "exec $(printf '%s' "$job" | sed 's/ "-fno-use-init-array"//')"
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# PS5 Vulkan Template - upload dist/<TITLE_ID>/ to the console.
|
||||
#
|
||||
# ps5/tools/deploy.sh [--all]
|
||||
#
|
||||
# PS5_Vulkan's deploy tool uploads what changed (eboot.bin always), over the
|
||||
# console's FTP server, with the address from PS5_Vulkan's .env.
|
||||
#
|
||||
# Copyright (C) 2026 Mihawk
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
ps5=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
root=$(dirname "$ps5")
|
||||
vulkan=$(cd -- "${PS5_VULKAN_DIR:-$root/../PS5_Vulkan}" && pwd)
|
||||
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$ps5/sce_sys/param.json")
|
||||
[[ -d $root/dist/$title_id ]] || { echo "no dist/$title_id: run ps5/tools/build.sh first" >&2; exit 2; }
|
||||
exec python3 "$vulkan/tools/deploy-title-folder.py" --always eboot.bin "$root/dist/$title_id" "$@"
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env bash
|
||||
# PS5 Vulkan Template - link the title and package its folder.
|
||||
#
|
||||
# link-title.sh BUILD_DIR PS5_VULKAN_DIR SDK OBJECT...
|
||||
#
|
||||
# Run by the build (ps5/CMakeLists.txt) once the objects are compiled. The link
|
||||
# is PS5_Vulkan's: its RADV release archive (RADV, ACO, NIR and Mesa's runtime,
|
||||
# built from PS5_Mesa), its link recipe (tools/radv-link.sh: the platform layer
|
||||
# of this title's SDK pin, the heap and thread wraps, the libc names bound to ps5_*),
|
||||
# its CRT, its native tool (ELF -> the console's fake SELF) and its libc.prx,
|
||||
# as its CTS title links them. The title folder, dist/<TITLE_ID>/, gets the
|
||||
# signed eboot.bin, sce_sys/ and the licences. OBJECT may be an archive.
|
||||
#
|
||||
# Copyright (C) 2026 Mihawk
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -euo pipefail
|
||||
[[ -n ${LINK_TRACE:-} ]] && set -x
|
||||
|
||||
work=$1 vulkan=$2 sdk=$3
|
||||
shift 3
|
||||
# CMake's link line for the game arrives as it would go to the compiler driver:
|
||||
# -Xlinker only introduces a linker argument there
|
||||
objects=()
|
||||
whole=false
|
||||
take() {
|
||||
local argument
|
||||
for argument in "$@"; do
|
||||
case $argument in
|
||||
# the platform archive is linked once, whole (ps5/cmake/ps5-game.cmake): the
|
||||
# copy among a library's dependencies would define its names a second time
|
||||
libps5_platform.a | */libps5_platform.a)
|
||||
$whole && objects+=("$argument")
|
||||
;;
|
||||
# ... and its groups go: the link below makes one group of everything
|
||||
-Xlinker | --start-group | --end-group) ;;
|
||||
# zlib is in the RADV archive already (Mesa's own, for its shader cache), whole:
|
||||
# the game's copy would define every name twice
|
||||
*/libz.a | -lz) ;;
|
||||
# a response file: Ninja's for a long line, and the game's objects in
|
||||
# SwitchWaker's order (native/cmake/executable.cmake's objects.rsp). No
|
||||
# path here has a space in it.
|
||||
@*)
|
||||
# shellcheck disable=SC2046
|
||||
take $(<"${argument#@}")
|
||||
;;
|
||||
--whole-archive) whole=true; objects+=("$argument") ;;
|
||||
--no-whole-archive) whole=false; objects+=("$argument") ;;
|
||||
*) objects+=("$argument") ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
take "$@"
|
||||
ps5=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
root=$(dirname "$ps5")
|
||||
archive=${RADV_ARCHIVE:-$vulkan/.deps/native/radv-release/lib/libvulkan_radeon.ps5.a}
|
||||
export PS5_CLANG=${PS5_CLANG:-$(command -v clang || true)}
|
||||
tool="$vulkan/build/host/ps5-native-tool"
|
||||
native="$vulkan/tooling/native"
|
||||
param="$ps5/sce_sys/param.json"
|
||||
|
||||
for file in "$archive" "$tool" "$param" "$sdk/bin/prospero-lld" "$vulkan/tools/radv-link.sh" \
|
||||
"$vulkan/runtime/libc.prx"; do
|
||||
[[ -e $file ]] || { echo "missing: $file" >&2; exit 2; }
|
||||
done
|
||||
mkdir -p "$work/link/obj" "$work/link/stubs"
|
||||
cc() { PS5_PAYLOAD_SDK="$sdk" sh "$vulkan/tooling/prospero-clang18" "$@"; }
|
||||
|
||||
# The CRT, as in
|
||||
# PS5_Vulkan's CTS title.
|
||||
cc -std=c++20 -O2 -fno-exceptions -fno-rtti -c "$native/app_crt.cpp" -o "$work/link/obj/app_crt.o"
|
||||
# RADV calls AGC, which the SDK has no stubs for: these name its imports.
|
||||
stub() {
|
||||
local library=$1 source=$2
|
||||
cc -std=c11 -O2 -fPIC -c "$vulkan/$source" -o "$work/link/obj/${library}_stub.o"
|
||||
"$sdk/bin/prospero-lld" --shared -soname "${library}.prx" \
|
||||
-o "$work/link/stubs/${library}.so" "$work/link/obj/${library}_stub.o"
|
||||
}
|
||||
stub libSceAgc vendor/ps5/sdk/stubs/agc_canary_link_stub.c
|
||||
stub libSceAgcDriver vendor/ps5/sdk/stubs/agc_driver_canary_link_stub.c
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$vulkan/tools/radv-link.sh"
|
||||
radv_link_recipe "$vulkan" "$sdk" "$archive" || exit 2
|
||||
# localeconv in the C locale, '.' the decimal point (the console's gives none, and
|
||||
# tinygltf's JSON parser then read 0.62 as 0): the platform layer's since SDK fork
|
||||
# 6b63a2a (tools/setup-sdk.sh's pin is never older), bound by PS5_Vulkan's recipe
|
||||
# since its 6a6dfa6. With an older recipe the title binds it here, kept local as
|
||||
# the recipe keeps its bound names. The symbol listing is read whole before it
|
||||
# is searched: piped into grep -q under pipefail, llvm-nm's SIGPIPE made the
|
||||
# found symbol a miss about half the time once the archive grew (SDK 611893f).
|
||||
platform_has_localeconv=false
|
||||
grep -q " T ps5_localeconv$" <<<"$("$sdk/bin/llvm-nm" --defined-only "$sdk/target/lib/libps5platform.a" 2>/dev/null)" &&
|
||||
platform_has_localeconv=true
|
||||
if $platform_has_localeconv && [[ " ${radv_link_flags[*]} " != *" --defsym=localeconv=ps5_localeconv "* ]]; then
|
||||
printf '{\n local:\n localeconv;\n};\n' > "$work/link/localeconv-local.map"
|
||||
radv_link_flags+=(--defsym=localeconv=ps5_localeconv --version-script "$work/link/localeconv-local.map")
|
||||
fi
|
||||
# std::jthread and std::stop_token are libc++'s experimental library in the SDK's LLVM 18
|
||||
experimental=()
|
||||
[[ -f $sdk/target/lib/libc++experimental.a ]] && experimental=("$sdk/target/lib/libc++experimental.a")
|
||||
# libc names no module a title loads exports, which the libraries linked in
|
||||
# call: ps5/src/shims.c answers each as ps5dl_<name>, kept local like the
|
||||
# recipe's own bound names
|
||||
shims=(isatty getresuid getresgid dl_iterate_phdr fchown fork vfork setsid pathconf getnameinfo gai_strerror lstat)
|
||||
{ printf '{\n local:\n'; printf ' %s;\n' "${shims[@]}"; printf '};\n'; } > "$work/link/shims-local.map"
|
||||
for name in "${shims[@]}"; do
|
||||
radv_link_flags+=("--defsym=$name=ps5dl_$name")
|
||||
done
|
||||
radv_link_flags+=(--version-script "$work/link/shims-local.map")
|
||||
# The decompilation has a function dummy(), and so has one of the console's
|
||||
# modules (libSceHttp2's stub exports _Z5dummyv): the linker would export the
|
||||
# game's to stand in for it. The game's stays its own.
|
||||
printf '{\n local:\n _Z5dummyv;\n};\n' > "$work/link/game-local.map"
|
||||
radv_link_flags+=(--version-script "$work/link/game-local.map")
|
||||
# A thread_local declared in a header without its initialiser is reached through a
|
||||
# wrapper that calls the variable's initialisation function (_ZTH<name>) if one
|
||||
# exists: a weak reference. The game's are all constant-initialised, so none
|
||||
# exists, and where another system leaves the reference null a title's link asks
|
||||
# the console's modules for it. Each such name that nothing defines is null here.
|
||||
names=$("$sdk/bin/llvm-nm" --no-sort "${objects[@]/#--*/}" 2>/dev/null | awk '$NF ~ /^_ZTH/ { print $(NF-1), $NF }' | sort -u || true)
|
||||
tls_null=$(comm -23 <(awk '$1 == "w" || $1 == "U" { print $2 }' <<<"$names" | sort -u) \
|
||||
<(awk '$1 != "w" && $1 != "U" { print $2 }' <<<"$names" | sort -u))
|
||||
if [[ -n $tls_null ]]; then
|
||||
# ... and local, as the bound names above are: a title exports nothing
|
||||
{ printf '{\n local:\n'; printf ' %s;\n' $tls_null; printf '};\n'; } > "$work/link/tls-null-local.map"
|
||||
for name in $tls_null; do
|
||||
radv_link_flags+=("--defsym=$name=0")
|
||||
done
|
||||
radv_link_flags+=(--version-script "$work/link/tls-null-local.map")
|
||||
fi
|
||||
[[ -n $tls_null ]] && echo "link-title.sh: $(wc -w <<<"$tls_null") thread_local initialisation references are null" >&2
|
||||
"$sdk/bin/prospero-lld" "${radv_linker_script[@]}" --eh-frame-hdr "${radv_link_flags[@]}" \
|
||||
--version-script "$native/app-symbols.map" --exclude-libs=ALL \
|
||||
-e _start -o "$work/link/llvm-pie.elf" \
|
||||
"$work/link/obj/app_crt.o" --start-group "${objects[@]}" "${experimental[@]}" --end-group \
|
||||
"$work/link/stubs/libSceAgc.so" "$work/link/stubs/libSceAgcDriver.so" \
|
||||
"${radv_link_inputs[@]}" \
|
||||
--as-needed "$sdk"/target/lib/*.so
|
||||
# A title whose localeconv is still the console's reads every glTF fraction as 0
|
||||
# (black materials, models scaled to nothing): refuse it here, not on the console
|
||||
if $platform_has_localeconv &&
|
||||
grep -q " U localeconv$" <<<"$("$sdk/bin/llvm-nm" "$work/link/llvm-pie.elf" 2>/dev/null)"; then
|
||||
echo "link-title.sh: localeconv is not bound to the platform layer's ps5_localeconv" >&2
|
||||
exit 1
|
||||
fi
|
||||
# A title loads neither libkernel_sys's exports nor libScePosixForWebKit's: an
|
||||
# import only their stubs define links, and is null at run time, so its first call
|
||||
# jumps to address 0 (PS5_RetroArch's strcasestr did; RADV imports readlink and
|
||||
# mkstemp). PS5_Vulkan's recipe binds those the platform layer has: refuse a
|
||||
# title that still imports one, here rather than on the console.
|
||||
null_imports=$(comm -23 \
|
||||
<("$sdk/bin/llvm-nm" -D --undefined-only "$work/link/llvm-pie.elf" |
|
||||
awk '$1 == "U" { sub(/@.*/, "", $2); print $2 }' | sort -u) \
|
||||
<(for library in "$sdk"/target/lib/*.so "$work/link/stubs/libSceAgc.so" "$work/link/stubs/libSceAgcDriver.so"; do
|
||||
case ${library##*/} in libkernel_sys.so | libScePosixForWebKit.so) continue ;; esac
|
||||
"$sdk/bin/llvm-nm" -D --defined-only "$library" 2>/dev/null | awk '{ print $NF }'
|
||||
done | sort -u))
|
||||
if [[ -n $null_imports ]]; then
|
||||
echo "link-title.sh: imports that no module a title loads exports (null at run time): ${null_imports//$'\n'/ }" >&2
|
||||
echo "link-title.sh: bind them to the platform layer (PS5_Vulkan's tools/radv-link.sh)" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$tool" link --in "$work/link/llvm-pie.elf" --out "$work/eboot.elf.new" \
|
||||
--stub-dir "$sdk/target/lib" --stub "$work/link/stubs/libSceAgc.so" \
|
||||
--stub "$work/link/stubs/libSceAgcDriver.so" --module-sdk 0x02000009 \
|
||||
--companion-sdk 0x08050001 --file-name eboot.elf
|
||||
|
||||
# The title folder
|
||||
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$param")
|
||||
app="$root/dist/$title_id"
|
||||
mkdir -p "$app/sce_sys" "$app/sce_module"
|
||||
"$tool" self --sign --in "$work/eboot.elf.new" --out "$app/eboot.bin" --magic 0x1D3D154F
|
||||
"$tool" self --inspect --file "$app/eboot.bin" > /dev/null
|
||||
cp "$param" "$app/sce_sys/param.json"
|
||||
for asset in icon0.png pic0.dds pic1.dds snd0.at9; do
|
||||
[[ -f $ps5/sce_sys/$asset ]] && cp "$ps5/sce_sys/$asset" "$app/sce_sys/$asset"
|
||||
done
|
||||
(cd "$vulkan/runtime" && sha256sum --check --strict --quiet libc.prx.sha256)
|
||||
cp "$vulkan/runtime/libc.prx" "$app/sce_module/libc.prx"
|
||||
mv "$work/eboot.elf.new" "$work/eboot.elf"
|
||||
# The linked ELF of the title last packaged, for tools/run.sh to symbolise a crash with
|
||||
# (the game's build tree may be on the VM's own disk)
|
||||
mkdir -p "$root/build"
|
||||
cp "$work/link/llvm-pie.elf" "$root/build/last-title.elf"
|
||||
archive_revision=$(sed -n 's/^revision: //p' "$(dirname "$(dirname "$archive")")/PROVENANCE.txt" 2>/dev/null || true)
|
||||
printf '==> %s: %s (eboot.bin %s bytes; RADV %s)\n' "$title_id" "$app" \
|
||||
"$(stat -c %s "$app/eboot.bin")" "${archive_revision:0:11}"
|
||||
# The list of render pipelines the game uses, compiled while it starts: pipeline
|
||||
# keys recorded from runs of the game, no game data (docs/SWITCH_BUILD.md)
|
||||
upstream="$root/vendor/switchwaker"
|
||||
cp "$upstream/native/data/initial_pipeline_cache.db" "$app/initial_pipeline_cache.db"
|
||||
# The DualSense's symbols for the game's button prompts, once there are any (ps5/art/glyphs)
|
||||
rm -rf "$app/res"
|
||||
if compgen -G "$ps5/art/glyphs/*.png" > /dev/null; then
|
||||
mkdir -p "$app/res/ps5-glyphs"
|
||||
cp "$ps5"/art/glyphs/*.png "$app/res/ps5-glyphs/"
|
||||
fi
|
||||
# The shader caches a console filled on a first start (ps5/shader-cache: Dawn's
|
||||
# cache and the driver's), copied into place by a start that finds none
|
||||
# (ps5/src/startup.c): every console has the same GPU and this title's driver, so
|
||||
# the first start need not compile three thousand pipelines again
|
||||
if [[ -f $ps5/shader-cache/dawn_cache.db ]]; then
|
||||
mkdir -p "$app/res/shader-cache"
|
||||
cp -R "$ps5/shader-cache/." "$app/res/shader-cache/"
|
||||
fi
|
||||
# The licences beside eboot.bin
|
||||
mkdir -p "$app/licenses"
|
||||
cp "$upstream/LICENSE" "$app/licenses/SwitchWaker-LICENSE.txt"
|
||||
cp "$upstream/game/LICENSE" "$app/licenses/SwitchWaker-game-LICENSE.txt"
|
||||
cp "$upstream/native/dsp_hle/LICENSE" "$app/licenses/Dolphin-DSPHLE-LICENSE.txt"
|
||||
cp "$upstream/THIRD_PARTY.md" "$app/licenses/SwitchWaker-THIRD_PARTY.md"
|
||||
cp "$root/.deps/aurora/LICENSE" "$app/licenses/Aurora-LICENSE.txt"
|
||||
cp "$vulkan/LICENSE" "$app/licenses/PS5_Vulkan-LICENSE.txt"
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# Wind Waker for the PlayStation 5 - a picture as home-screen art (sce_sys/pic0.dds, pic1.dds).
|
||||
#
|
||||
# python3 ps5/tools/png-to-title-dds.py PICTURE OUTPUT.dds
|
||||
#
|
||||
# pic0.dds is the background behind the title on the home screen; pic1.dds is the
|
||||
# splash while it starts (seen on the console, 2026-10-10). The picture is scaled
|
||||
# to 3840x2160 and written in the one form the console is known to show: DDS, DX10
|
||||
# header, BC7_UNORM, one level, straight alpha.
|
||||
#
|
||||
# The BC7 mode 6 encoder and the DDS header are PS5 RetroArch's
|
||||
# (tools/make-title-art.py), unchanged:
|
||||
# Copyright (C) 2026 Mihawk-99
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
WIDTH, HEIGHT = 3840, 2160
|
||||
|
||||
WEIGHTS = np.array([0, 4, 9, 13, 17, 21, 26, 30, 34, 38, 43, 47, 51, 55, 60, 64], dtype=np.int64)
|
||||
|
||||
|
||||
def quantise(e: np.ndarray) -> np.ndarray:
|
||||
"""Endpoint colours to the 7 stored bits (the p-bit is 1, for alpha 255)."""
|
||||
return np.clip(np.round((e - 1.0) / 2.0), 0, 127).astype(np.int64)
|
||||
|
||||
|
||||
def palette(q0: np.ndarray, q1: np.ndarray) -> np.ndarray:
|
||||
"""The 16 colours two quantised endpoints give, (blocks, 16, 3)."""
|
||||
e0, e1 = (q0 * 2 + 1)[:, None, :], (q1 * 2 + 1)[:, None, :]
|
||||
w = WEIGHTS[None, :, None]
|
||||
return ((64 - w) * e0 + w * e1 + 32) >> 6
|
||||
|
||||
|
||||
def choose(px: np.ndarray, q0: np.ndarray, q1: np.ndarray):
|
||||
"""Each pixel's best index for these endpoints, and the block's error."""
|
||||
pal = palette(q0, q1)
|
||||
err = ((px[:, :, None, :] - pal[:, None, :, :]) ** 2).sum(-1)
|
||||
idx = err.argmin(-1)
|
||||
return idx, np.take_along_axis(err, idx[..., None], -1)[..., 0].sum(-1)
|
||||
|
||||
|
||||
def encode_blocks(px: np.ndarray) -> bytes:
|
||||
"""BC7 mode 6 for blocks of 16 RGB pixels (blocks, 16, 3), alpha 255."""
|
||||
pf = px.astype(np.float64)
|
||||
mean = pf.mean(1)
|
||||
cen = pf - mean[:, None, :]
|
||||
cov = np.einsum('nki,nkj->nij', cen, cen)
|
||||
axis = np.ones((len(px), 3)) / np.sqrt(3.0)
|
||||
for _ in range(8):
|
||||
axis = np.einsum('nij,nj->ni', cov, axis)
|
||||
norm = np.sqrt((axis * axis).sum(1, keepdims=True))
|
||||
axis = np.where(norm > 1e-9, axis / np.maximum(norm, 1e-9), 1.0 / np.sqrt(3.0))
|
||||
proj = np.einsum('nki,ni->nk', cen, axis)
|
||||
q0 = quantise(mean + proj.min(1, keepdims=True) * axis)
|
||||
q1 = quantise(mean + proj.max(1, keepdims=True) * axis)
|
||||
idx, err = choose(px, q0, q1)
|
||||
# One least-squares refit of the endpoints to the chosen indices
|
||||
w = WEIGHTS[idx].astype(np.float64) / 64.0
|
||||
a, b, c = ((1 - w) ** 2).sum(1), ((1 - w) * w).sum(1), (w * w).sum(1)
|
||||
d0 = ((1 - w)[..., None] * pf).sum(1)
|
||||
d1 = (w[..., None] * pf).sum(1)
|
||||
det = a * c - b * b
|
||||
ok = det > 1e-9
|
||||
safe = np.where(ok, det, 1.0)[:, None]
|
||||
r0 = quantise(np.where(ok[:, None], (c[:, None] * d0 - b[:, None] * d1) / safe, mean))
|
||||
r1 = quantise(np.where(ok[:, None], (a[:, None] * d1 - b[:, None] * d0) / safe, mean))
|
||||
idx2, err2 = choose(px, r0, r1)
|
||||
better = (err2 < err)[:, None]
|
||||
q0, q1 = np.where(better, r0, q0), np.where(better, r1, q1)
|
||||
idx = np.where(better, idx2, idx)
|
||||
# The first pixel's index is stored in 3 bits: swap the ends if it needs 4
|
||||
swap = idx[:, 0] >= 8
|
||||
q0, q1 = np.where(swap[:, None], q1, q0), np.where(swap[:, None], q0, q1)
|
||||
idx = np.where(swap[:, None], 15 - idx, idx)
|
||||
|
||||
lo = np.full(len(px), 1 << 6, dtype=np.uint64) # mode 6
|
||||
hi = np.zeros(len(px), dtype=np.uint64)
|
||||
pos = 7
|
||||
|
||||
def put(value: np.ndarray, bits: int):
|
||||
nonlocal pos
|
||||
for bit in range(bits):
|
||||
v = ((value >> bit) & 1).astype(np.uint64)
|
||||
if pos < 64:
|
||||
lo[:] |= v << np.uint64(pos)
|
||||
else:
|
||||
hi[:] |= v << np.uint64(pos - 64)
|
||||
pos += 1
|
||||
|
||||
for ch in range(3):
|
||||
put(q0[:, ch], 7)
|
||||
put(q1[:, ch], 7)
|
||||
alpha = np.full(len(px), 127, dtype=np.int64)
|
||||
put(alpha, 7)
|
||||
put(alpha, 7)
|
||||
one = np.ones(len(px), dtype=np.int64)
|
||||
put(one, 1) # P0
|
||||
put(one, 1) # P1
|
||||
put(idx[:, 0], 3)
|
||||
for i in range(1, 16):
|
||||
put(idx[:, i], 4)
|
||||
out = np.empty(len(px) * 2, dtype="<u8")
|
||||
out[0::2], out[1::2] = lo, hi
|
||||
return out.tobytes()
|
||||
|
||||
|
||||
def bc7(img: np.ndarray) -> bytes:
|
||||
"""The picture as BC7 blocks, row of blocks by row of blocks."""
|
||||
rgb = np.clip(np.round(img), 0, 255).astype(np.int64)
|
||||
by, bx = HEIGHT // 4, WIDTH // 4
|
||||
blocks = rgb.reshape(by, 4, bx, 4, 3).transpose(0, 2, 1, 3, 4).reshape(by * bx, 16, 3)
|
||||
step = bx * 16
|
||||
return b"".join(encode_blocks(blocks[i:i + step]) for i in range(0, len(blocks), step))
|
||||
|
||||
|
||||
def dds_header() -> bytes:
|
||||
"""The DDS header of the launcher art the console has shown: DX10, BC7_UNORM
|
||||
(98), 2D, one level, straight alpha."""
|
||||
DDSD_CAPS, DDSD_HEIGHT, DDSD_WIDTH, DDSD_PIXELFORMAT = 0x1, 0x2, 0x4, 0x1000
|
||||
DDSD_MIPMAPCOUNT, DDSD_LINEARSIZE = 0x20000, 0x80000
|
||||
flags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_MIPMAPCOUNT | DDSD_LINEARSIZE
|
||||
linear = WIDTH * HEIGHT # BC7: 16 bytes a 4x4 block, one byte a pixel
|
||||
header = struct.pack("<4sIIIIIII", b"DDS ", 124, flags, HEIGHT, WIDTH, linear, 1, 1)
|
||||
header += b"\0" * 44 # reserved
|
||||
header += struct.pack("<II4sIIIII", 32, 0x4, b"DX10", 0, 0, 0, 0, 0)
|
||||
header += struct.pack("<IIIII", 0x1000, 0, 0, 0, 0) # caps: texture
|
||||
header += struct.pack("<IIIII", 98, 3, 0, 1, 1) # BC7_UNORM, TEXTURE2D, array 1, straight alpha
|
||||
assert len(header) == 148
|
||||
return header
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print("usage: png-to-title-dds.py PICTURE OUTPUT.dds", file=sys.stderr)
|
||||
return 2
|
||||
from PIL import Image
|
||||
picture = Image.open(sys.argv[1]).convert("RGB").resize((WIDTH, HEIGHT), Image.LANCZOS)
|
||||
data = dds_header() + bc7(np.asarray(picture, dtype=np.float64))
|
||||
Path(sys.argv[2]).write_bytes(data)
|
||||
print(f"{sys.argv[2]}: {len(data)} bytes")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - launch the title on the console and follow its log.
|
||||
#
|
||||
# ps5/tools/run.sh launch, and capture klog until the title ends
|
||||
# RUN_TIMEOUT=120 ps5/tools/run.sh give up after that many seconds
|
||||
#
|
||||
# PS5_Vulkan's run tool checks that the console runs nothing, launches the title
|
||||
# through the control payload (ps5vkctl), captures klog until the title's
|
||||
# "ends" line, a crash record or the title's exit, and symbolises a crash. The
|
||||
# log is kept in klog/<run>/klog.log.
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -euo pipefail
|
||||
ps5=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
root=$(dirname "$ps5")
|
||||
vulkan=$(cd -- "${PS5_VULKAN_DIR:-$root/../PS5_Vulkan}" && pwd)
|
||||
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$ps5/sce_sys/param.json")
|
||||
# klog's prefix: the title's name, as a regular expression
|
||||
prefix=$(python3 -c 'import json,re,sys; print(re.escape("[" + json.load(open(sys.argv[1]))["localizedParameters"]["en-US"]["titleName"] + "]"))' "$ps5/sce_sys/param.json")
|
||||
run_dir="$root/klog/$(date +%Y%m%d-%H%M%S)"
|
||||
mkdir -p "$run_dir"
|
||||
exec python3 "$vulkan/tools/run-title.py" "$title_id" \
|
||||
--until "$prefix ends" --timeout "${RUN_TIMEOUT:-86400}" \
|
||||
--echo "$prefix|fatal|FAILED|Fatal" \
|
||||
--elf "$root/build/last-title.elf" --output "$run_dir/klog.log"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - Dawn at the revision Aurora pins, patched for the console.
|
||||
#
|
||||
# ps5/tools/setup-dawn.sh fetch it into .deps/dawn (once a revision), print the path
|
||||
#
|
||||
# Aurora (.deps/aurora, tools/setup-source.sh) names its Dawn in
|
||||
# cmake/AuroraDependencyVersions.cmake (AURORA_DAWN_REF, a commit of
|
||||
# encounter/dawn). This fetches that commit and Dawn's own third-party sources
|
||||
# (tools/fetch_dawn_dependencies.py), then applies ps5/patches/dawn.patch: the
|
||||
# PS5 as a platform, the RADV linked into the title in place of a Vulkan
|
||||
# library, and the console's surface (VK_KHR_display). Dawn's Abseil gets
|
||||
# ps5/patches/dawn-abseil.patch (its raw log writes with write(), not syscall()).
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
dawn="$root/.deps/dawn"
|
||||
versions="$root/.deps/aurora/cmake/AuroraDependencyVersions.cmake"
|
||||
[[ -f $versions ]] || bash "$root/ps5/tools/setup-source.sh"
|
||||
revision=$(sed -n 's/^_aurora_dependency_version(AURORA_DAWN_REF "\([0-9a-f]\{40\}\)".*/\1/p' "$versions")
|
||||
[[ -n $revision ]] || { echo "setup-dawn.sh: no AURORA_DAWN_REF in $versions" >&2; exit 2; }
|
||||
patch="$root/ps5/patches/dawn.patch"
|
||||
abseil_patch="$root/ps5/patches/dawn-abseil.patch"
|
||||
stamp="$revision $(cat "$patch" "$abseil_patch" | sha256sum | cut -d' ' -f1)"
|
||||
|
||||
if [[ ! -f $dawn/.ps5-dawn || $(<"$dawn/.ps5-dawn") != "$stamp" ]]; then
|
||||
{
|
||||
if [[ ! -d $dawn/.git ]]; then
|
||||
mkdir -p "$dawn"
|
||||
git -C "$dawn" init -q
|
||||
git -C "$dawn" remote add origin https://github.com/encounter/dawn.git
|
||||
fi
|
||||
if [[ $(git -C "$dawn" rev-parse -q --verify HEAD 2>/dev/null) != "$revision" ]]; then
|
||||
git -C "$dawn" fetch -q --depth 1 origin "$revision"
|
||||
git -C "$dawn" checkout -q -f FETCH_HEAD
|
||||
(cd "$dawn" && python3 tools/fetch_dawn_dependencies.py)
|
||||
fi
|
||||
git -C "$dawn" checkout -q -f HEAD -- .
|
||||
git -C "$dawn" apply --whitespace=nowarn "$patch"
|
||||
git -C "$dawn/third_party/abseil-cpp" checkout -q -f HEAD -- .
|
||||
git -C "$dawn/third_party/abseil-cpp" apply --whitespace=nowarn "$abseil_patch"
|
||||
} >&2
|
||||
echo "$stamp" > "$dawn/.ps5-dawn"
|
||||
fi
|
||||
echo "$dawn"
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# PS5 Vulkan Template - the payload SDK this title builds with, at a pinned revision.
|
||||
#
|
||||
# ps5/tools/setup-sdk.sh install it into .deps/native/ps5-payload-sdk (once a revision)
|
||||
#
|
||||
# The SDK is my fork, ../PS5_PayloadSDK: the upstream release with the PS5
|
||||
# platform layer (libps5platform.a, include/ps5platform) installed over it. The
|
||||
# pinned revision is exported with git archive, so a build never depends on the
|
||||
# fork's working tree, and the SDK directory records it, as PS5 RetroArch and
|
||||
# PS5 vkQuake pin theirs. RADV's archive, its link recipe and the packaging
|
||||
# tools stay PS5_Vulkan's.
|
||||
#
|
||||
# Copyright (C) 2026 Mihawk
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
cache="$root/.deps/native"
|
||||
sdk="$cache/ps5-payload-sdk"
|
||||
sdk_fork="${PS5_PAYLOAD_SDK_FORK:-$root/../PS5_PayloadSDK}"
|
||||
# 611893f: /data through the Lapy daemon (ps5platform/elevation.h), an opt-in no sample calls.
|
||||
# Never older than 6b63a2a, the platform layer's localeconv in the C locale (ps5_localeconv).
|
||||
sdk_revision=611893fc25ef3e138f5bc2c90d829d8b58174cd2
|
||||
|
||||
if [[ ! -f $sdk/.ps5-sdk-revision || $(<"$sdk/.ps5-sdk-revision") != "$sdk_revision" ]]; then
|
||||
git -C "$sdk_fork" cat-file -e "$sdk_revision^{commit}" 2>/dev/null || {
|
||||
echo "the payload SDK fork at $sdk_fork does not have $sdk_revision" >&2
|
||||
exit 2
|
||||
}
|
||||
mkdir -p "$cache"
|
||||
sdk_tree=$(mktemp -d)
|
||||
git -C "$sdk_fork" archive "$sdk_revision" | tar -x -C "$sdk_tree"
|
||||
bash "$sdk_tree/platform/tools/setup-sdk.sh" "$sdk" "$sdk_revision" "$cache" >&2
|
||||
rm -rf -- "$sdk_tree"
|
||||
fi
|
||||
[[ -x $sdk/bin/prospero-lld && -d $sdk/target/include ]] || { echo "the pinned PS5 payload SDK is incomplete" >&2; exit 2; }
|
||||
echo "$sdk"
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - SDL3 at the release Aurora pins, patched for the console.
|
||||
#
|
||||
# ps5/tools/setup-sdl.sh fetch it into .deps/SDL (once a release), print the path
|
||||
#
|
||||
# Aurora names its SDL3 in cmake/AuroraDependencyVersions.cmake (AURORA_SDL3_REF,
|
||||
# a release tag). ps5/patches/sdl.patch registers the console's video, audio and
|
||||
# joystick drivers, which live in ps5/sdl/, and builds them when the toolchain
|
||||
# says PS5.
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
sdl="$root/.deps/SDL"
|
||||
versions="$root/.deps/aurora/cmake/AuroraDependencyVersions.cmake"
|
||||
[[ -f $versions ]] || bash "$root/ps5/tools/setup-source.sh"
|
||||
tag=$(sed -n 's/^_aurora_dependency_version(AURORA_SDL3_REF "refs\/tags\/\([^"]*\)".*/\1/p' "$versions")
|
||||
[[ -n $tag ]] || { echo "setup-sdl.sh: no AURORA_SDL3_REF in $versions" >&2; exit 2; }
|
||||
patch="$root/ps5/patches/sdl.patch"
|
||||
stamp="$tag $(sha256sum "$patch" | cut -d' ' -f1)"
|
||||
|
||||
if [[ ! -f $sdl/.ps5-sdl || $(<"$sdl/.ps5-sdl") != "$stamp" ]]; then
|
||||
{
|
||||
if [[ ! -d $sdl/.git ]]; then
|
||||
git clone -q --depth 1 -b "$tag" https://github.com/libsdl-org/SDL.git "$sdl"
|
||||
elif [[ $(git -C "$sdl" describe --tags --exact-match 2>/dev/null) != "$tag" ]]; then
|
||||
git -C "$sdl" fetch -q --depth 1 origin "refs/tags/$tag:refs/tags/$tag"
|
||||
git -C "$sdl" checkout -q -f "$tag"
|
||||
fi
|
||||
git -C "$sdl" checkout -q -f HEAD -- .
|
||||
git -C "$sdl" apply --whitespace=nowarn "$patch"
|
||||
} >&2
|
||||
echo "$stamp" > "$sdl/.ps5-sdl"
|
||||
fi
|
||||
echo "$sdl"
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - the sources the build needs, with the console's changes.
|
||||
#
|
||||
# ps5/tools/setup-source.sh check out and patch everything; safe before every build
|
||||
#
|
||||
# SwitchWaker (vendor/switchwaker) is a submodule at an upstream revision. What
|
||||
# the console needs changed in it is ps5/patches/switchwaker.patch, every hunk
|
||||
# behind __PROSPERO__. Three things SwitchWaker fetches for itself are fetched
|
||||
# here instead, into .deps/, so the build needs no network and no disc:
|
||||
#
|
||||
# .deps/aurora Aurora at SwitchWaker's pin (native/cmake/Aurora.cmake),
|
||||
# with SwitchWaker's own patches (native/patches/aurora) and
|
||||
# then ps5/patches/aurora.patch (the console's surface)
|
||||
# .deps/recompcore Dolphin's DSP HLE sources (native/tools/fetch_recompcore.sh)
|
||||
# .deps/assets/GZLE01 stub asset headers (native/tools/gen_assets.sh --stubs):
|
||||
# names and sizes only, from the decompilation's symbol
|
||||
# list. The arrays are read from the player's disc when
|
||||
# the title starts (docs/RUNTIME_ASSETS.md), so nothing of
|
||||
# the game is compiled in. Needs PyYAML.
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
upstream="$root/vendor/switchwaker"
|
||||
deps="$root/.deps"
|
||||
|
||||
[[ -f $upstream/native/CMakeLists.txt ]] || git -C "$root" submodule update --init vendor/switchwaker >&2
|
||||
|
||||
patch="$root/ps5/patches/switchwaker.patch"
|
||||
if [[ -s $patch ]] && ! git -C "$upstream" apply --reverse --check "$patch" 2>/dev/null; then
|
||||
git -C "$upstream" apply --check "$patch" || {
|
||||
echo "setup-source.sh: switchwaker.patch does not apply to $upstream (a different upstream revision, or local edits: git -C vendor/switchwaker diff > ps5/patches/switchwaker.patch)" >&2
|
||||
exit 2
|
||||
}
|
||||
git -C "$upstream" apply --whitespace=nowarn "$patch"
|
||||
echo "setup-source.sh: applied switchwaker.patch" >&2
|
||||
fi
|
||||
|
||||
# Aurora
|
||||
aurora="$deps/aurora"
|
||||
pin=$(sed -n 's/^set(COS_AURORA_COMMIT "\([0-9a-f]\{40\}\)".*/\1/p' "$upstream/native/cmake/Aurora.cmake")
|
||||
[[ -n $pin ]] || { echo "setup-source.sh: no COS_AURORA_COMMIT in native/cmake/Aurora.cmake" >&2; exit 2; }
|
||||
stamp="$pin $(cat "$upstream"/native/patches/aurora/*.patch "$root/ps5/patches/aurora.patch" | sha256sum | cut -d' ' -f1)"
|
||||
if [[ ! -f $aurora/.ps5-aurora || $(<"$aurora/.ps5-aurora") != "$stamp" ]]; then
|
||||
{
|
||||
if [[ ! -d $aurora/.git ]]; then
|
||||
mkdir -p "$aurora"
|
||||
git -C "$aurora" init -q
|
||||
git -C "$aurora" remote add origin https://github.com/encounter/aurora.git
|
||||
fi
|
||||
if [[ $(git -C "$aurora" rev-parse -q --verify HEAD 2>/dev/null) != "$pin" ]]; then
|
||||
git -C "$aurora" fetch -q --depth 1 origin "$pin"
|
||||
git -C "$aurora" checkout -q -f FETCH_HEAD
|
||||
fi
|
||||
git -C "$aurora" checkout -q -f HEAD -- .
|
||||
git -C "$aurora" clean -q -fd
|
||||
for each in "$upstream"/native/patches/aurora/*.patch "$root/ps5/patches/aurora.patch"; do
|
||||
git -C "$aurora" apply --whitespace=nowarn "$each" || {
|
||||
echo "setup-source.sh: ${each##*/} does not apply to Aurora ${pin:0:7}" >&2
|
||||
exit 2
|
||||
}
|
||||
done
|
||||
} >&2
|
||||
echo "$stamp" > "$aurora/.ps5-aurora"
|
||||
fi
|
||||
|
||||
# Dolphin's DSP HLE
|
||||
[[ -f $deps/recompcore/Source/Core/Core/HW/DSPHLE/UCodes/UCodes.cpp ]] ||
|
||||
bash "$upstream/native/tools/fetch_recompcore.sh" "$deps/recompcore" >&2
|
||||
|
||||
# Stub asset headers
|
||||
if [[ ! -f $deps/assets/GZLE01/include/assets/cos_assets.h ]]; then
|
||||
bash "$upstream/native/tools/gen_assets.sh" --stubs --decomp "$deps/decomp" --out "$deps/assets/GZLE01" >&2
|
||||
fi
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wind Waker for the PlayStation 5 - run a build tool inside the Linux VM, from macOS.
|
||||
#
|
||||
# ps5/tools/vm.sh build ps5/tools/build.sh (the game)
|
||||
# ps5/tools/vm.sh deploy ps5/tools/deploy.sh (FTP to the console)
|
||||
# ps5/tools/vm.sh run ps5/tools/run.sh (launch and follow klog)
|
||||
#
|
||||
# The stack (PS5_Vulkan with its RADV archive, PS5_Mesa, PS5_PayloadSDK) lives in
|
||||
# the Lima VM "ps5" under ~/ps5, built with LLVM 22 (~/ps5/env.sh); this folder is
|
||||
# mounted there at the same path. The game's build tree goes on the VM's own disk
|
||||
# (~/ps5/build/PS5_SwitchWaker-game): the shared folder is several times slower for
|
||||
# three thousand objects. The console's address is PS5_HOST in the VM's
|
||||
# ~/ps5/PS5_Vulkan/.env.
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
tool=${1:?usage: vm.sh build|deploy|run [arguments]}
|
||||
shift
|
||||
exec limactl shell ps5 -- bash -lc '. ~/ps5/env.sh; export PS5_VULKAN_DIR=~/ps5/PS5_Vulkan PS5_PAYLOAD_SDK_FORK=~/ps5/PS5_PayloadSDK PS5_BUILD_DIR=${PS5_BUILD_DIR:-$HOME/ps5/build/PS5_SwitchWaker-game}; cd "$1"; tool=$2; shift 2; exec bash "ps5/tools/$tool.sh" "$@"' _ "$root" "$tool" "$@"
|
||||