commit 3c17fb22d6bb439d54d482a0507652af99e6ccf3 Author: Gary Riches Date: Mon Oct 12 15:56:25 2026 +0100 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9ffbf6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +build/ +dist/ +klog/ +.deps/ +gamedata/ +*.log +imgui.ini +# never: disc images +*.iso +*.ciso +*.gcm +*.rvz +*.gci diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4dd32dd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/switchwaker"] + path = vendor/switchwaker + url = https://github.com/centollOS/SwitchWaker.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 0000000..95d3066 --- /dev/null +++ b/LICENSING.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..37afc1f --- /dev/null +++ b/README.md @@ -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. diff --git a/licenses/GPL-3.0.txt b/licenses/GPL-3.0.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/licenses/GPL-3.0.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/licenses/MIT.txt b/licenses/MIT.txt new file mode 100644 index 0000000..2c4cfd5 --- /dev/null +++ b/licenses/MIT.txt @@ -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. diff --git a/licenses/Zlib.txt b/licenses/Zlib.txt new file mode 100644 index 0000000..60ac574 --- /dev/null +++ b/licenses/Zlib.txt @@ -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. diff --git a/ps5/art/glyphs/tex1_128x32_523124ff1f96d4bc_e7820c87ebbc4db4_9.png b/ps5/art/glyphs/tex1_128x32_523124ff1f96d4bc_e7820c87ebbc4db4_9.png new file mode 100644 index 0000000..f5381e4 Binary files /dev/null and b/ps5/art/glyphs/tex1_128x32_523124ff1f96d4bc_e7820c87ebbc4db4_9.png differ diff --git a/ps5/art/glyphs/tex1_16x16_0bbde935468955d4_0.png b/ps5/art/glyphs/tex1_16x16_0bbde935468955d4_0.png new file mode 100644 index 0000000..3e9970a Binary files /dev/null and b/ps5/art/glyphs/tex1_16x16_0bbde935468955d4_0.png differ diff --git a/ps5/art/glyphs/tex1_16x16_a954694dda754d44_0.png b/ps5/art/glyphs/tex1_16x16_a954694dda754d44_0.png new file mode 100644 index 0000000..cd4aea7 Binary files /dev/null and b/ps5/art/glyphs/tex1_16x16_a954694dda754d44_0.png differ diff --git a/ps5/art/glyphs/tex1_16x16_c78319a187997148_0.png b/ps5/art/glyphs/tex1_16x16_c78319a187997148_0.png new file mode 100644 index 0000000..2b731b7 Binary files /dev/null and b/ps5/art/glyphs/tex1_16x16_c78319a187997148_0.png differ diff --git a/ps5/art/glyphs/tex1_16x16_f2ecddd16b5f554a_0.png b/ps5/art/glyphs/tex1_16x16_f2ecddd16b5f554a_0.png new file mode 100644 index 0000000..4214505 Binary files /dev/null and b/ps5/art/glyphs/tex1_16x16_f2ecddd16b5f554a_0.png differ diff --git a/ps5/art/glyphs/tex1_16x16_fd8f1a5a26d95e59_0.png b/ps5/art/glyphs/tex1_16x16_fd8f1a5a26d95e59_0.png new file mode 100644 index 0000000..b8c4328 Binary files /dev/null and b/ps5/art/glyphs/tex1_16x16_fd8f1a5a26d95e59_0.png differ diff --git a/ps5/art/glyphs/tex1_40x40_08daf72f6acc9478_1acd6c849fcbc8cc_9.png b/ps5/art/glyphs/tex1_40x40_08daf72f6acc9478_1acd6c849fcbc8cc_9.png new file mode 100644 index 0000000..0eec134 Binary files /dev/null and b/ps5/art/glyphs/tex1_40x40_08daf72f6acc9478_1acd6c849fcbc8cc_9.png differ diff --git a/ps5/art/glyphs/tex1_48x24_c07e25b9bca86b7e_ce3f64d5da874475_8.png b/ps5/art/glyphs/tex1_48x24_c07e25b9bca86b7e_ce3f64d5da874475_8.png new file mode 100644 index 0000000..8b41db3 Binary files /dev/null and b/ps5/art/glyphs/tex1_48x24_c07e25b9bca86b7e_ce3f64d5da874475_8.png differ diff --git a/ps5/art/icon0-source.png b/ps5/art/icon0-source.png new file mode 100644 index 0000000..df02533 Binary files /dev/null and b/ps5/art/icon0-source.png differ diff --git a/ps5/art/kenney/License.txt b/ps5/art/kenney/License.txt new file mode 100644 index 0000000..e618524 --- /dev/null +++ b/ps5/art/kenney/License.txt @@ -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 \ No newline at end of file diff --git a/ps5/art/kenney/playstation_button_circle_outline.png b/ps5/art/kenney/playstation_button_circle_outline.png new file mode 100644 index 0000000..908b3ab Binary files /dev/null and b/ps5/art/kenney/playstation_button_circle_outline.png differ diff --git a/ps5/art/kenney/playstation_button_cross_outline.png b/ps5/art/kenney/playstation_button_cross_outline.png new file mode 100644 index 0000000..2b079b2 Binary files /dev/null and b/ps5/art/kenney/playstation_button_cross_outline.png differ diff --git a/ps5/art/kenney/playstation_button_square_outline.png b/ps5/art/kenney/playstation_button_square_outline.png new file mode 100644 index 0000000..a41b517 Binary files /dev/null and b/ps5/art/kenney/playstation_button_square_outline.png differ diff --git a/ps5/art/kenney/playstation_button_triangle_outline.png b/ps5/art/kenney/playstation_button_triangle_outline.png new file mode 100644 index 0000000..cc2e316 Binary files /dev/null and b/ps5/art/kenney/playstation_button_triangle_outline.png differ diff --git a/ps5/art/kenney/playstation_stick_r.png b/ps5/art/kenney/playstation_stick_r.png new file mode 100644 index 0000000..17133d9 Binary files /dev/null and b/ps5/art/kenney/playstation_stick_r.png differ diff --git a/ps5/art/kenney/playstation_trigger_l2_outline.png b/ps5/art/kenney/playstation_trigger_l2_outline.png new file mode 100644 index 0000000..0957ef9 Binary files /dev/null and b/ps5/art/kenney/playstation_trigger_l2_outline.png differ diff --git a/ps5/art/kenney/playstation_trigger_r1_outline.png b/ps5/art/kenney/playstation_trigger_r1_outline.png new file mode 100644 index 0000000..9a68678 Binary files /dev/null and b/ps5/art/kenney/playstation_trigger_r1_outline.png differ diff --git a/ps5/art/kenney/playstation_trigger_r2_outline.png b/ps5/art/kenney/playstation_trigger_r2_outline.png new file mode 100644 index 0000000..2f545ac Binary files /dev/null and b/ps5/art/kenney/playstation_trigger_r2_outline.png differ diff --git a/ps5/art/make-glyphs.py b/ps5/art/make-glyphs.py new file mode 100644 index 0000000..aa5d7ee --- /dev/null +++ b/ps5/art/make-glyphs.py @@ -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_x_[_]_.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"))) diff --git a/ps5/art/make-title-art.sh b/ps5/art/make-title-art.sh new file mode 100755 index 0000000..b51806b --- /dev/null +++ b/ps5/art/make-title-art.sh @@ -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" diff --git a/ps5/art/pic-source.png b/ps5/art/pic-source.png new file mode 100644 index 0000000..160327f Binary files /dev/null and b/ps5/art/pic-source.png differ diff --git a/ps5/cmake/ps5-game.cmake b/ps5/cmake/ps5-game.cmake new file mode 100644 index 0000000..3b82a06 --- /dev/null +++ b/ps5/cmake/ps5-game.cmake @@ -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($<$:-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 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} @${CMAKE_BINARY_DIR}/cos_exe/objects.rsp --whole-archive ${ps5_platform_archive} --no-whole-archive ") +# (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) diff --git a/ps5/nod/include/nod.h b/ps5/nod/include/nod.h new file mode 100644 index 0000000..3818d76 --- /dev/null +++ b/ps5/nod/include/nod.h @@ -0,0 +1,621 @@ +#ifndef NOD_H +#define NOD_H + +#include +#include +#include +/** + * 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 */ diff --git a/ps5/nod/nod.cpp b/ps5/nod/nod.cpp new file mode 100644 index 0000000..b00b3af --- /dev/null +++ b/ps5/nod/nod.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 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(n); + } + return static_cast(done); + } + + // The disc + int64_t readAt(uint64_t offset, void* out, size_t len) { + if (offset >= size) { + return 0; + } + len = static_cast(std::min(len, size - offset)); + auto* p = static_cast(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(std::min(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(n) < part) { + memset(p + done + n, 0, part - static_cast(n)); // a stored block cut short + } + } + done += part; + } + return static_cast(done); + } + + bool readExact(uint64_t offset, void* out, size_t len) { + return readAt(offset, out, len) == static_cast(len); + } +}; + +struct Partition { + std::vector 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; + std::shared_ptr 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& 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(offset), static_cast(size)); + return false; + } + out.resize(static_cast(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 loadPartition(Disc& disc) { + auto partition = std::make_shared(); + 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(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(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->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(length); + + // CISO: "CISO", the block size, then a byte a block that says whether it is stored + { + std::vector 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 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(user); + if (fseeko(f, static_cast(offset), SEEK_SET) != 0) { + return -1; + } + const size_t n = fread(buffer, 1, len, f); + return n == 0 && ferror(f) ? -1 : static_cast(n); + }, + .stream_len = [](void* user) -> int64_t { + auto* f = static_cast(user); + if (fseeko(f, 0, SEEK_END) != 0) { + return -1; + } + return static_cast(ftello(f)); + }, + .close = [](void* user) { fclose(static_cast(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(std::min(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(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(handle->position); + break; + case 2: + base = static_cast(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(base + offset); + return static_cast(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" diff --git a/ps5/patches/aurora.patch b/ps5/patches/aurora.patch new file mode 100644 index 0000000..89e606f --- /dev/null +++ b/ps5/patches/aurora.patch @@ -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 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 desc = std::make_shared(); ++ desc->window = &output; ++ return std::move(desc); + #elif defined(SDL_PLATFORM_LINUX) + const char* driver = SDL_GetCurrentVideoDriver(); + if (SDL_strcmp(driver, "wayland") == 0) { diff --git a/ps5/patches/dawn-abseil.patch b/ps5/patches/dawn-abseil.patch new file mode 100644 index 0000000..5f68768 --- /dev/null +++ b/ps5/patches/dawn-abseil.patch @@ -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 + #define ABSL_HAVE_SYSCALL_WRITE 1 + #define ABSL_LOW_LEVEL_WRITE_SUPPORTED 1 diff --git a/ps5/patches/dawn.patch b/ps5/patches/dawn.patch new file mode 100644 index 0000000..ef56a8d --- /dev/null +++ b/ps5/patches/dawn.patch @@ -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 + + #include +-#elif DAWN_PLATFORM_IS(SWITCH) ++#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5) + #include + #endif + +@@ -161,7 +161,7 @@ std::optional GetExecutablePath() { + std::optional GetExecutablePath() { + return {}; + } +-#elif DAWN_PLATFORM_IS(SWITCH) ++#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5) + std::optional GetExecutablePath() { + return {}; + } +@@ -219,7 +219,7 @@ std::optional GetModulePath() { + std::optional GetModulePath() { + return {}; + } +-#elif DAWN_PLATFORM_IS(SWITCH) ++#elif DAWN_PLATFORM_IS(SWITCH) || DAWN_PLATFORM_IS(PS5) + std::optional 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> 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(); + 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 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 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( + 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 CreateViSurfaceNN = nullptr; + #endif // defined(VK_USE_PLATFORM_VI_NN) + ++ // KHR_display, the PS5's only surface (VideoOut) ++ VkFn GetPhysicalDeviceDisplayPropertiesKHR = nullptr; ++ VkFn GetDisplayModePropertiesKHR = nullptr; ++ VkFn 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 diff --git a/ps5/patches/sdl.patch b/ps5/patches/sdl.patch new file mode 100644 index 0000000..dddc007 --- /dev/null +++ b/ps5/patches/sdl.patch @@ -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 diff --git a/ps5/patches/switchwaker.patch b/ps5/patches/switchwaker.patch new file mode 100644 index 0000000..bba223e --- /dev/null +++ b/ps5/patches/switchwaker.patch @@ -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 + #include + + #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 + #include + #include ++#include + #include + #include + #include +@@ -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 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 + #include + #include ++#include + #include + #include + #include +@@ -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 ++#include ++#endif + #if defined(__APPLE__) + #include + #include +@@ -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/), 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 + #include + ++#include // 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_x__.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 diff --git a/ps5/sce_sys/icon0.png b/ps5/sce_sys/icon0.png new file mode 100644 index 0000000..8419a5b Binary files /dev/null and b/ps5/sce_sys/icon0.png differ diff --git a/ps5/sce_sys/param.json b/ps5/sce_sys/param.json new file mode 100644 index 0000000..b247c58 --- /dev/null +++ b/ps5/sce_sys/param.json @@ -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": "" +} diff --git a/ps5/sce_sys/pic0.dds b/ps5/sce_sys/pic0.dds new file mode 100644 index 0000000..f840006 Binary files /dev/null and b/ps5/sce_sys/pic0.dds differ diff --git a/ps5/sce_sys/pic1.dds b/ps5/sce_sys/pic1.dds new file mode 100644 index 0000000..f840006 Binary files /dev/null and b/ps5/sce_sys/pic1.dds differ diff --git a/ps5/sdl/SDL_ps5audio.c b/ps5/sdl/SDL_ps5audio.c new file mode 100644 index 0000000..484c9d5 --- /dev/null +++ b/ps5/sdl/SDL_ps5audio.c @@ -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 diff --git a/ps5/sdl/SDL_ps5joystick.c b/ps5/sdl/SDL_ps5joystick.c new file mode 100644 index 0000000..a0c94fa --- /dev/null +++ b/ps5/sdl/SDL_ps5joystick.c @@ -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 diff --git a/ps5/sdl/SDL_ps5video.c b/ps5/sdl/SDL_ps5video.c new file mode 100644 index 0000000..55c96ed --- /dev/null +++ b/ps5/sdl/SDL_ps5video.c @@ -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 + +#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 diff --git a/ps5/shader-cache/dawn_cache.db b/ps5/shader-cache/dawn_cache.db new file mode 100644 index 0000000..8140a9f Binary files /dev/null and b/ps5/shader-cache/dawn_cache.db differ diff --git a/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/index b/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/index new file mode 100644 index 0000000..4a43b75 Binary files /dev/null and b/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/index differ diff --git a/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/part0/mesa_cache.db b/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/part0/mesa_cache.db new file mode 100644 index 0000000..d95677d Binary files /dev/null and b/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/part0/mesa_cache.db differ diff --git a/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/part0/mesa_cache.idx b/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/part0/mesa_cache.idx new file mode 100644 index 0000000..bcb3ee8 Binary files /dev/null and b/ps5/shader-cache/radv-shader-cache/mesa_shader_cache_db/part0/mesa_cache.idx differ diff --git a/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/index b/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/index new file mode 100644 index 0000000..4a43b75 Binary files /dev/null and b/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/index differ diff --git a/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/part0/mesa_cache.db b/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/part0/mesa_cache.db new file mode 100644 index 0000000..21d8104 Binary files /dev/null and b/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/part0/mesa_cache.db differ diff --git a/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/part0/mesa_cache.idx b/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/part0/mesa_cache.idx new file mode 100644 index 0000000..6aa4e88 Binary files /dev/null and b/ps5/shader-cache/radv-shader-cache/radv_builtin_shaders/part0/mesa_cache.idx differ diff --git a/ps5/src/platform.c b/ps5/src/platform.c new file mode 100644 index 0000000..faa86cf --- /dev/null +++ b/ps5/src/platform.c @@ -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 + +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/ps5/src/platform.h b/ps5/src/platform.h new file mode 100644 index 0000000..b1c9aeb --- /dev/null +++ b/ps5/src/platform.h @@ -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 +#include + +#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 diff --git a/ps5/src/shims.c b/ps5/src/shims.c new file mode 100644 index 0000000..548a37f --- /dev/null +++ b/ps5/src/shims.c @@ -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 + +/* 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); +} diff --git a/ps5/src/startup.c b/ps5/src/startup.c new file mode 100644 index 0000000..eee79ab --- /dev/null +++ b/ps5/src/startup.c @@ -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 +#include +#include +#include + +#include + +#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///: saves and settings. A title + * sees its own folder (/app0, which is /data/homebrew/) 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); +} diff --git a/ps5/tools/build.sh b/ps5/tools/build.sh new file mode 100755 index 0000000..a1f2c30 --- /dev/null +++ b/ps5/tools/build.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Wind Waker for the PlayStation 5 - build the game into dist//. +# +# 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}" diff --git a/ps5/tools/cc-init-array.sh b/ps5/tools/cc-init-array.sh new file mode 100755 index 0000000..1f315f6 --- /dev/null +++ b/ps5/tools/cc-init-array.sh @@ -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"//')" diff --git a/ps5/tools/deploy.sh b/ps5/tools/deploy.sh new file mode 100755 index 0000000..fdc9046 --- /dev/null +++ b/ps5/tools/deploy.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# PS5 Vulkan Template - upload dist// 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" "$@" diff --git a/ps5/tools/link-title.sh b/ps5/tools/link-title.sh new file mode 100755 index 0000000..26e114c --- /dev/null +++ b/ps5/tools/link-title.sh @@ -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//, 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_, 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) 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" diff --git a/ps5/tools/png-to-title-dds.py b/ps5/tools/png-to-title-dds.py new file mode 100644 index 0000000..87adf9f --- /dev/null +++ b/ps5/tools/png-to-title-dds.py @@ -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=" 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(" 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()) diff --git a/ps5/tools/run.sh b/ps5/tools/run.sh new file mode 100755 index 0000000..6775e59 --- /dev/null +++ b/ps5/tools/run.sh @@ -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//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" diff --git a/ps5/tools/setup-dawn.sh b/ps5/tools/setup-dawn.sh new file mode 100755 index 0000000..0411fb7 --- /dev/null +++ b/ps5/tools/setup-dawn.sh @@ -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" diff --git a/ps5/tools/setup-sdk.sh b/ps5/tools/setup-sdk.sh new file mode 100755 index 0000000..f0f507b --- /dev/null +++ b/ps5/tools/setup-sdk.sh @@ -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" diff --git a/ps5/tools/setup-sdl.sh b/ps5/tools/setup-sdl.sh new file mode 100755 index 0000000..b419cc5 --- /dev/null +++ b/ps5/tools/setup-sdl.sh @@ -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" diff --git a/ps5/tools/setup-source.sh b/ps5/tools/setup-source.sh new file mode 100755 index 0000000..cfdf5a9 --- /dev/null +++ b/ps5/tools/setup-source.sh @@ -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 diff --git a/ps5/tools/vm.sh b/ps5/tools/vm.sh new file mode 100755 index 0000000..60f7efd --- /dev/null +++ b/ps5/tools/vm.sh @@ -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" "$@" diff --git a/vendor/switchwaker b/vendor/switchwaker new file mode 160000 index 0000000..ba8d0a3 --- /dev/null +++ b/vendor/switchwaker @@ -0,0 +1 @@ +Subproject commit ba8d0a31446cd53ceaebd6b7269cca641e73bf4c