diff --git a/.gitignore b/.gitignore index b85ab43..4e9c87a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ build/ -bin/ -pipeline_cache.bin +bin/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 0a04128..79b8aa7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,165 +1,65 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 +GNU LESSER GENERAL PUBLIC LICENSE - 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. +Version 3, 29 June 2007 +Copyright © 2007 Free Software Foundation, Inc. - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - 0. Additional Definitions. +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. +0. Additional Definitions. - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. +As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. +“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. +An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". +A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. +The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. +The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. +1. Exception to Section 3 of the GNU GPL. - 1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. +2. Conveying Modified Versions. - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: - 2. Conveying Modified Versions. + a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or + b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: +3. Object Code Incorporating Material from Library Header Files. - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. + a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. + b) Accompany the object code with a copy of the GNU GPL and this license document. - 3. Object Code Incorporating Material from Library Header Files. +4. Combined Works. - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. + a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. + b) Accompany the Combined Work with a copy of the GNU GPL and this license document. + c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. + d) Do one of the following: + 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. + e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) - b) Accompany the object code with a copy of the GNU GPL and this license - document. +5. Combined Libraries. - 4. Combined Works. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. + b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. +6. Revised Versions of the GNU Lesser General Public License. - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. +Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. diff --git a/LICENSE.GPL b/LICENSE.GPL deleted file mode 100644 index f288702..0000000 --- a/LICENSE.GPL +++ /dev/null @@ -1,674 +0,0 @@ - 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/README.md b/README.md index fc81db2..c8e5ce2 100644 --- a/README.md +++ b/README.md @@ -222,13 +222,4 @@ See [examples/](examples/). Quick map: ## License -This library is licensed under the GNU Lesser General Public License, version 3 only (LGPL-3.0-only). See [LICENSE](LICENSE) for the license text. The LGPL supplements the GNU General Public License version 3, a copy of which is included as [LICENSE.GPL](LICENSE.GPL). - -The example code under [examples/](examples/) is licensed under the MIT license (see [examples/LICENSE](examples/LICENSE)), so it can be freely copied into your own projects. - -The [lib/](lib/) directory contains vendored third-party code (stb, Wayland protocol bindings) that remains under its own permissive licenses; see the headers of those files. - -## Copyright - -Copyright (C) 2026 Catcrafts® -catcrafts.net +LGPL 3.0. See per-file headers and `LICENSE`. diff --git a/additional/dom-env.js b/additional/dom-env.js index eda0273..e8fbff6 100644 --- a/additional/dom-env.js +++ b/additional/dom-env.js @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // JS bridge for the CRAFTER_GRAPHICS_WINDOW_DOM build of Crafter.Graphics. // Populates `window.crafter_webbuild_env` (same global as Crafter.CppDOM @@ -109,60 +125,10 @@ function setStyle(cookie, stylePtr, styleLen) { const el = __jsmemory.get(cookie); if (el) el.style.cssText = __readUtf8(stylePtr, styleLen); } -// NOTE: setProperty is a CSS custom-property setter (el.style.setProperty), -// NOT setAttribute. The name is historical and misleading; the attribute -// functions below are the ones you want for href/src/disabled/aria-*. function setProperty(cookie, propPtr, propLen, valPtr, valLen) { const el = __jsmemory.get(cookie); if (el) el.style.setProperty(__readUtf8(propPtr, propLen), __readUtf8(valPtr, valLen)); } -// Real attribute access. Without these the only way to change an href, src, -// disabled or aria-* was to re-render the parent's innerHTML, which destroys -// every descendant (and their event listeners) to change one string. -function setAttribute(cookie, namePtr, nameLen, valPtr, valLen) { - const el = __jsmemory.get(cookie); - if (el) el.setAttribute(__readUtf8(namePtr, nameLen), __readUtf8(valPtr, valLen)); -} -function removeAttribute(cookie, namePtr, nameLen) { - const el = __jsmemory.get(cookie); - if (el) el.removeAttribute(__readUtf8(namePtr, nameLen)); -} -// Returns 0 for "absent" so the caller can tell it apart from an attribute -// that is present with an empty value (`disabled=""`, `alt=""`) — which -// matters because for boolean attributes presence alone is the signal. -function getAttribute(cookie, namePtr, nameLen) { - const el = __jsmemory.get(cookie); - if (!el) return 0; - const v = el.getAttribute(__readUtf8(namePtr, nameLen)); - if (v === null) return 0; - return __writeUtf8(v); -} -function hasAttribute(cookie, namePtr, nameLen) { - const el = __jsmemory.get(cookie); - if (!el) return false; - return el.hasAttribute(__readUtf8(namePtr, nameLen)); -} -// Checkbox / radio state. el.value on a checkbox returns "on" regardless of -// whether it is ticked, so GetValue cannot express this. -function getChecked(cookie) { - const el = __jsmemory.get(cookie); - return !!(el && el.checked); -} -function setChecked(cookie, checked) { - const el = __jsmemory.get(cookie); - if (el) el.checked = !!checked; -} -// Move keyboard focus. AddFocusListener could observe focus but nothing -// could set it, which makes an error summary ("jump to the first invalid -// field") impossible to implement accessibly. -function focusElement(cookie) { - const el = __jsmemory.get(cookie); - if (el && el.focus) el.focus(); -} -function blurElement(cookie) { - const el = __jsmemory.get(cookie); - if (el && el.blur) el.blur(); -} function addClass(cookie, namePtr, nameLen) { const el = __jsmemory.get(cookie); if (el) el.classList.add(__readUtf8(namePtr, nameLen)); @@ -211,25 +177,12 @@ function __dpr() { return window.crafter_dpr || window.devicePixelRatio || 1; } -// `preventDefault` is opt-in per listener (the C++ Add*Listener overloads -// take it as a trailing argument). Two reasons it has to exist here rather -// than being something C++ can do after the fact: -// * the wasm callback runs synchronously inside the dispatch, but the -// decision has to be made before the browser acts on the event, and -// there is no handle to the event object on the C++ side; -// * intercepting a click on a real — the whole basis of -// client-side routing over crawlable links — is impossible without it. -// Cancelling happens BEFORE the wasm call so a trap or exception inside the -// handler can't leave the default action to fire anyway. -// `{ passive: false }` is required or the browser silently ignores -// preventDefault on wheel (and warns); harmless on the other kinds. function __makeMouseListenerPair(kind, eventName, exportName) { return { - add(cookie, id, preventDefault) { + add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { - if (preventDefault) event.preventDefault(); const s = __dpr(); __wasm()[exportName](id, event.clientX * s, event.clientY * s, @@ -238,7 +191,7 @@ function __makeMouseListenerPair(kind, eventName, exportName) { event.altKey, event.ctrlKey, event.shiftKey, event.metaKey); }; __listenerHandlers.set(`${cookie}-${id}-${kind}`, handler); - el.addEventListener(eventName, handler, preventDefault ? { passive: false } : undefined); + el.addEventListener(eventName, handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); @@ -251,18 +204,17 @@ function __makeMouseListenerPair(kind, eventName, exportName) { } function __makeKeyListenerPair(kind, eventName, exportName) { return { - add(cookie, id, preventDefault) { + add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { - if (preventDefault) event.preventDefault(); const keyPtr = __writeUtf8(event.key || ""); __wasm()[exportName](id, keyPtr, event.keyCode, event.altKey, event.ctrlKey, event.shiftKey, event.metaKey); __wasm().WasmFree(keyPtr); }; __listenerHandlers.set(`${cookie}-${id}-${kind}`, handler); - el.addEventListener(eventName, handler, preventDefault ? { passive: false } : undefined); + el.addEventListener(eventName, handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); @@ -403,10 +355,9 @@ const __scrollPair = { } }; const __wheelPair = { - add(cookie, id, preventDefault) { + add(cookie, id) { const el = __jsmemory.get(cookie); if (!el) return; const handler = (event) => { - if (preventDefault) event.preventDefault(); const s = __dpr(); __wasm().ExecuteWheelHandler(id, event.deltaX, event.deltaY, event.deltaZ, event.deltaMode, @@ -415,10 +366,7 @@ const __wheelPair = { event.altKey, event.ctrlKey, event.shiftKey, event.metaKey); }; __listenerHandlers.set(`${cookie}-${id}-wheel`, handler); - // passive:false is mandatory here — Chrome treats wheel as passive by - // default and would otherwise drop the preventDefault with a console - // warning rather than an error. - el.addEventListener("wheel", handler, preventDefault ? { passive: false } : undefined); + el.addEventListener("wheel", handler); }, remove(cookie, id) { const el = __jsmemory.get(cookie); @@ -687,45 +635,9 @@ function removePopStateListener(id) { if (h) window.removeEventListener("popstate", h); __listenerHandlers.delete(`popstate-${id}`); } -// replaceState mirrors pushState but rewrites the current history entry -// instead of adding one. Needed for anything that changes the URL without -// being a distinct "back" destination — filter/sort state, canonicalising a -// sloppy incoming URL, or recording scroll position on the entry you are -// about to leave. -function replaceState(dataPtr, dataLen, titlePtr, titleLen, urlPtr, urlLen) { - const dataStr = __readUtf8(dataPtr, dataLen); - const titleStr = __readUtf8(titlePtr, titleLen); - const urlStr = __readUtf8(urlPtr, urlLen); - let parsed; - try { parsed = JSON.parse(dataStr); } catch { parsed = null; } - window.history.replaceState(parsed, titleStr, urlStr); -} function getPathName() { return __writeUtf8(window.location.pathname); } -// location.search / .hash, including their leading "?" / "#" so an empty -// query and "?" are distinguishable, and so the result can be concatenated -// straight back onto a path. pathname alone left query strings completely -// unreachable from wasm, which made ?page= / ?sort= impossible to read. -function getSearch() { - return __writeUtf8(window.location.search); -} -function getHash() { - return __writeUtf8(window.location.hash); -} -function getHref() { - return __writeUtf8(window.location.href); -} -// Full-page navigation, including cross-origin. pushState deliberately -// cannot leave the origin, so this is the only way for a wasm app to hand -// the user off to an external URL (an OAuth consent screen, a hosted -// payment page). `replace` omits the current page from history so Back -// doesn't return to a stale checkout. -function navigate(urlPtr, urlLen, replace) { - const url = __readUtf8(urlPtr, urlLen); - if (replace) window.location.replace(url); - else window.location.assign(url); -} // ─── Gamepad polling helper ─────────────────────────────────────────── // @@ -763,8 +675,6 @@ Object.assign(window.crafter_webbuild_env, { // DOM lookup / creation / mutation freeJs, getElementById, createElement, getBody, setInnerHTML, setStyle, setProperty, - setAttribute, removeAttribute, getAttribute, hasAttribute, - getChecked, setChecked, focusElement, blurElement, addClass, removeClass, toggleClass, hasClass, deleteElement, getValue, setValue, @@ -803,8 +713,7 @@ Object.assign(window.crafter_webbuild_env, { clipboardSetText, clipboardGetText, // History - pushState, replaceState, addPopStateListener, removePopStateListener, - getPathName, getSearch, getHash, getHref, navigate, + pushState, addPopStateListener, removePopStateListener, getPathName, // Gamepad gamepadPollConnected, gamepadPollDisconnected, diff --git a/additional/dom-webgpu.js b/additional/dom-webgpu.js index 5bee542..c35456b 100644 --- a/additional/dom-webgpu.js +++ b/additional/dom-webgpu.js @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter.Graphics WebGPU bridge — DOM mode UI compute pipeline. @@ -57,7 +54,6 @@ function stub(name) { "wgpuRegisterMeshBLAS", "wgpuRegisterMeshBLASDeviceAabbs", "wgpuRefitMeshBLASDeviceAabbs", "wgpuLoadRTPipeline", "wgpuDispatchRT", "wgpuBuildTLAS", "wgpuLoadComputePipeline", "wgpuDispatchCompute", - "wgpuSetCanvasMount", ]) { // Read-write ints don't need a stub-throw; return 0 for the size queries. e[n] = n.endsWith("Width") || n.endsWith("Height") @@ -89,21 +85,6 @@ canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;display: document.body.style.margin = "0"; document.body.appendChild(canvas); -// Optional mount target set via wgpuSetCanvasMount() (below). When null the -// canvas is a full-viewport fixed layer (the original, unchanged default); -// when set, the canvas is reparented into that element and sized to it. -let mountEl = null; - -// Upper bound on the canvas backing store, in pixels. Infinity until an RT -// dispatch derives the real budget from the device's buffer limits (see -// wgpuDispatchRT): the wavefront ray buffers scale with W·H·raysPerPixel, -// and a 256 MiB maxBufferSize (Firefox's baseline) overflows at ~0.5 Mpx -// for 4 rays/pixel — far below a HiDPI 16:9 mount — failing not at the -// createBuffer call but as an uncapturable device OOM at submit. The -// mounted canvas is CSS-pinned to width:100%/height:100%, so a clamped -// backing store upscales on screen instead of shrinking. -let maxCanvasPixels = Infinity; - function syncCanvasSize() { // Canvas pixel size = CSS size × devicePixelRatio so the GPU draws // at physical pixel resolution on HiDPI displays — otherwise the @@ -117,23 +98,8 @@ function syncCanvasSize() { // share the physical-pixel coordinate space with window.width/.height. const dpr = window.devicePixelRatio || 1; window.crafter_dpr = dpr; - // Mounted → track the host element's box; unmounted → the viewport. - let cssW, cssH; - if (mountEl) { - const r = mountEl.getBoundingClientRect(); - cssW = r.width; - cssH = r.height; - } else { - cssW = window.innerWidth; - cssH = window.innerHeight; - } - let w = Math.max(1, Math.round(cssW * dpr)); - let h = Math.max(1, Math.round(cssH * dpr)); - if (w * h > maxCanvasPixels) { - const s = Math.sqrt(maxCanvasPixels / (w * h)); - w = Math.max(1, Math.floor(w * s)); - h = Math.max(1, Math.floor(h * s)); - } + const w = Math.max(1, Math.round(window.innerWidth * dpr)); + const h = Math.max(1, Math.round(window.innerHeight * dpr)); if (canvas.width !== w) canvas.width = w; if (canvas.height !== h) canvas.height = h; return { w, h }; @@ -290,17 +256,12 @@ struct UIDispatchHeader { @group(1) @binding(0) var outTex : texture_storage_2d; @group(1) @binding(1) var prevTex : texture_2d; -// Renderer-reserved high flag bit (mirrors ui-shared.glsl::UI_FLAG_CLIP). -const UI_FLAG_CLIP : u32 = 0x80000000u; - fn uiResolvePixel(coord: vec2) -> bool { if (coord.x >= hdr.surfaceW || coord.y >= hdr.surfaceH) { return false; } - if ((hdr.flags & UI_FLAG_CLIP) != 0u) { - let fx = f32(coord.x); let fy = f32(coord.y); - if (fx < hdr.clipX || fy < hdr.clipY) { return false; } - if (fx >= hdr.clipX + hdr.clipW) { return false; } - if (fy >= hdr.clipY + hdr.clipH) { return false; } - } + let fx = f32(coord.x); let fy = f32(coord.y); + if (fx < hdr.clipX || fy < hdr.clipY) { return false; } + if (fx >= hdr.clipX + hdr.clipW) { return false; } + if (fy >= hdr.clipY + hdr.clipH) { return false; } return true; } @@ -600,40 +561,6 @@ const env = window.crafter_webbuild_env; env.wgpuGetCanvasWidth = () => canvas.width; env.wgpuGetCanvasHeight = () => canvas.height; -// Reparent the render canvas into a host DOM element so a Crafter.Graphics -// scene can render inline inside an app's own layout instead of as a -// full-page layer. `idPtr`/`idLen` is a UTF-8 wasm string: -// - non-empty id of an existing element → move the canvas into it, -// position it to fill it (absolute inset:0), and size the render -// surface to that element's box (syncCanvasSize picks it up per frame). -// - empty string → detach back to and hide it (display:none), so -// the host page is a plain DOM document again. -// The element is made position:relative if it is otherwise static, so the -// absolutely-positioned canvas anchors to it. Pointer events pass through -// (the inline use cases are non-interactive display surfaces). -env.wgpuSetCanvasMount = (idPtr, idLen) => { - const id = idLen > 0 - ? new TextDecoder().decode(memU8().subarray(idPtr, idPtr + idLen)) - : ""; - if (!id) { - mountEl = null; - if (canvas.parentNode !== document.body) document.body.appendChild(canvas); - canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;display:none;"; - return; - } - const el = document.getElementById(id); - if (!el) { - console.warn(`[crafter-wgpu] wgpuSetCanvasMount: no element #${id}`); - return; - } - mountEl = el; - if (getComputedStyle(el).position === "static") el.style.position = "relative"; - if (canvas.parentNode !== el) el.appendChild(canvas); - canvas.style.cssText = - "position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none;"; - ensureSized(); -}; - env.wgpuCreateBuffer = (byteSize) => { const h = newHandle(); const buf = device.createBuffer({ @@ -681,7 +608,7 @@ env.wgpuWriteBufferRange = (handle, dstByteOffset, srcPtr, byteSize) => { const READBACK_IDLE = 0; const READBACK_PENDING = 1; const READBACK_READY = 2; -const readbacks = new Map(); // device-buffer handle → { staging, capacity, readBytes, state, pendingData } +const readbacks = new Map(); // device-buffer handle → { staging, size, state, pendingData } // Readbacks scheduled this frame that still need their mapAsync kicked // off — done after the frame's queue.submit so the map waits for the // compute writes that wrote to `buf` to finish, not just the standalone @@ -691,30 +618,21 @@ const pendingReadbackMaps = []; env.wgpuReadbackEnqueue = (handle, byteSize, resetBytes) => { const buf = buffers.get(handle); if (!buf) return; + const aligned = (byteSize + 3) & ~3; const resetAligned = resetBytes > 0 ? ((resetBytes + 3) & ~3) : 0; let rb = readbacks.get(handle); if (!rb) { - // Size the staging buffer to the FULL device-buffer capacity, not - // this first call's byteSize: a prefix readback (byteCount < size) - // is allowed to vary call-to-call, so the staging must cover any - // later full-size drain without re-allocating. - const capacity = Math.max(16, (buf.size + 3) & ~3); rb = { staging: device.createBuffer({ - size: capacity, + size: Math.max(16, aligned), usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST, }), - capacity, - readBytes: 0, // bytes copied/mapped by the in-flight enqueue + size: aligned, state: READBACK_IDLE, pendingData: null, }; readbacks.set(handle, rb); } - // Copy/map only the requested prefix — that's the whole point of the - // byteCount path: skip the over-provisioned tail GPU→staging→wasm. - const aligned = Math.min((byteSize + 3) & ~3, rb.capacity); - rb.readBytes = aligned; if (rb.state !== READBACK_IDLE) { // Previous map still in flight (or has data nobody polled yet); // skip this enqueue AND the paired reset. Events written by the @@ -758,9 +676,8 @@ env.wgpuReadbackEnqueue = (handle, byteSize, resetBytes) => { if (resetAligned > 0) enc.clearBuffer(buf, 0, resetAligned); queue.submit([enc.finish()]); rb.state = READBACK_PENDING; - const mapBytes = rb.readBytes; - rb.staging.mapAsync(GPUMapMode.READ, 0, mapBytes).then(() => { - rb.pendingData = new Uint8Array(rb.staging.getMappedRange(0, mapBytes)).slice(); + rb.staging.mapAsync(GPUMapMode.READ).then(() => { + rb.pendingData = new Uint8Array(rb.staging.getMappedRange()).slice(); rb.staging.unmap(); rb.state = READBACK_READY; }).catch(e => { @@ -1088,9 +1005,8 @@ env.wgpuFrameEnd = () => { // writes (not pre-substep state). while (pendingReadbackMaps.length > 0) { const rb = pendingReadbackMaps.pop(); - const mapBytes = rb.readBytes; - rb.staging.mapAsync(GPUMapMode.READ, 0, mapBytes).then(() => { - rb.pendingData = new Uint8Array(rb.staging.getMappedRange(0, mapBytes)).slice(); + rb.staging.mapAsync(GPUMapMode.READ).then(() => { + rb.pendingData = new Uint8Array(rb.staging.getMappedRange()).slice(); rb.staging.unmap(); rb.state = READBACK_READY; }).catch(e => { @@ -3526,28 +3442,6 @@ env.wgpuDispatchRT = (pipelineHandle, pushPtr, pushBytes, } const W = state.width, H = state.height; const depth = Math.max(1, maxDepth | 0); - // Clamp the render resolution to what the wavefront buffers can hold. - // The payload store is the largest per-ray cost (2 regions × - // WF_PAYLOAD_BYTES) and must fit both maxBufferSize and one storage - // binding; the indirect TRACE/SHADE dispatch additionally bounds rays - // at maxComputeWorkgroupsPerDimension×64. When the canvas is over - // budget, tighten maxCanvasPixels and sit this frame out — the next - // wgpuFrameBegin's ensureSized() shrinks the backing store (CSS keeps - // the on-screen size) and rendering resumes within budget. - const rpp = Math.max(1, raysPerPixel | 0); - const rayBudget = Math.min( - Math.floor(Math.min(device.limits.maxBufferSize, - device.limits.maxStorageBufferBindingSize) - / (2 * WF_PAYLOAD_BYTES)), - device.limits.maxComputeWorkgroupsPerDimension * 64); - const pixelBudget = Math.max(1, Math.floor(rayBudget / rpp)); - if (pixelBudget < maxCanvasPixels) maxCanvasPixels = pixelBudget; - if (W * H > maxCanvasPixels) { - console.warn(`[crafter-wgpu] ${W}x${H} at ${rpp} rays/px overflows the ` - + `wavefront budget (maxBufferSize ${device.limits.maxBufferSize}); ` - + `reducing render resolution to ~${maxCanvasPixels}px`); - return; - } const wf = ensureWavefrontBuffers(W, H, raysPerPixel); const cap = wf.cap; // per-bounce ray capacity = raysPerPixel·W·H diff --git a/examples/CustomShader/inverse-circle.comp.glsl b/examples/CustomShader/inverse-circle.comp.glsl index 00ada77..b9c1620 100644 --- a/examples/CustomShader/inverse-circle.comp.glsl +++ b/examples/CustomShader/inverse-circle.comp.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Custom UI compute shader. Demonstrates the Tier 1 dispatch path: // the user defines their own item struct, writes their own GLSL alongside // the standard shaders (sharing the same UIDispatchHeader contract via diff --git a/examples/CustomShader/inverse-circle.comp.wgsl b/examples/CustomShader/inverse-circle.comp.wgsl index 39cf753..81341ee 100644 --- a/examples/CustomShader/inverse-circle.comp.wgsl +++ b/examples/CustomShader/inverse-circle.comp.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // DOM-mode port of inverse-circle.comp.glsl. Inverts RGB inside each // user-supplied circle; passes through every other pixel so the // ping-pong carries the prior dispatch's scene forward. diff --git a/examples/CustomShader/main.cpp b/examples/CustomShader/main.cpp index 960d002..4d798c7 100644 --- a/examples/CustomShader/main.cpp +++ b/examples/CustomShader/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Tier 1 demo: a user-authored compute shader dispatched alongside the // standard ones. The custom shader inverts RGB in the area covered by a // list of circles. The mouse-tracking circle moves; two static ones sit diff --git a/examples/CustomShader/project.cpp b/examples/CustomShader/project.cpp index eae0218..f9d2f29 100644 --- a/examples/CustomShader/project.cpp +++ b/examples/CustomShader/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/Decompression/main.cpp b/examples/Decompression/main.cpp index e316a7a..e8cc543 100644 --- a/examples/Decompression/main.cpp +++ b/examples/Decompression/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // End-to-end demo of GPU asset decompression via VK_EXT_memory_decompression. // // Walks the full compressed-asset pipeline: diff --git a/examples/Decompression/project.cpp b/examples/Decompression/project.cpp index 51918c4..f69ec56 100644 --- a/examples/Decompression/project.cpp +++ b/examples/Decompression/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/HDRBloom/blur.comp.wgsl b/examples/HDRBloom/blur.comp.wgsl index 542ff95..b4a570a 100644 --- a/examples/HDRBloom/blur.comp.wgsl +++ b/examples/HDRBloom/blur.comp.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom blur pass (PlainComputeShader). Box-blurs the thresholded bloom // mip into a second rgba16float target — demonstrating an N-target float // chain: this pass SAMPLES the texture the threshold pass wrote (storage diff --git a/examples/HDRBloom/closesthit.wgsl b/examples/HDRBloom/closesthit.wgsl index cf48b80..ef83cf8 100644 --- a/examples/HDRBloom/closesthit.wgsl +++ b/examples/HDRBloom/closesthit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom closest-hit (runs in SHADE). Half the cubes are "emitters" that // accumulate a strongly super-1.0 radiance (the HDR signal a bloom pass // extracts); the rest are dim Lambert-shaded fillers near/below 1.0 that the diff --git a/examples/HDRBloom/composite.comp.wgsl b/examples/HDRBloom/composite.comp.wgsl index 79969b1..f1c893a 100644 --- a/examples/HDRBloom/composite.comp.wgsl +++ b/examples/HDRBloom/composite.comp.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom composite (UI custom shader, dispatched via UIRenderer). Runs // inside the per-frame UI compute pass, so it owns the ping-pong `out` // texture that gets blitted to the canvas — the app's composite→swapchain diff --git a/examples/HDRBloom/main.cpp b/examples/HDRBloom/main.cpp index 1431c5b..6495c19 100644 --- a/examples/HDRBloom/main.cpp +++ b/examples/HDRBloom/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom — cross-backend-shaped HDR post-process on the WebGPU/DOM // backend, exercising the primitives added for issue #27: // diff --git a/examples/HDRBloom/miss.wgsl b/examples/HDRBloom/miss.wgsl index c059935..fcaae4f 100644 --- a/examples/HDRBloom/miss.wgsl +++ b/examples/HDRBloom/miss.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom miss (runs in SHADE). Dark, sub-threshold background so the // bloom pass only picks up the bright cubes, not the sky. fn miss_main(ray: RayDesc, payload: ptr) { diff --git a/examples/HDRBloom/project.cpp b/examples/HDRBloom/project.cpp index e2632d4..071adba 100644 --- a/examples/HDRBloom/project.cpp +++ b/examples/HDRBloom/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/HDRBloom/raygen.wgsl b/examples/HDRBloom/raygen.wgsl index 548620a..ad45486 100644 --- a/examples/HDRBloom/raygen.wgsl +++ b/examples/HDRBloom/raygen.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom raygen (runs in GENERATE). Host-driven pinhole camera at // @group(3) (groups 0..2 are reserved by the wavefront pipeline: // 0 = WfParams, 1 = data heaps, 2 = indirect args). Primary rays only — diff --git a/examples/HDRBloom/threshold.comp.wgsl b/examples/HDRBloom/threshold.comp.wgsl index 2b830a7..8003f86 100644 --- a/examples/HDRBloom/threshold.comp.wgsl +++ b/examples/HDRBloom/threshold.comp.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // HDRBloom threshold pass (PlainComputeShader). Reads the linear HDR scene // (rgba16float, written by the RT RESOLVE stage), keeps only the radiance // above 1.0, and writes it into the bloom mip (also rgba16float). This is diff --git a/examples/HelloDom/main.cpp b/examples/HelloDom/main.cpp index f400ff3..503e373 100644 --- a/examples/HelloDom/main.cpp +++ b/examples/HelloDom/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* HelloDom — exercises every public surface of the DOM partition that absorbed Crafter.CppDOM: diff --git a/examples/HelloDom/project.cpp b/examples/HelloDom/project.cpp index 8fa17c8..2a55444 100644 --- a/examples/HelloDom/project.cpp +++ b/examples/HelloDom/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* HelloDom — the minimum DOM-mode example. Build with: crafter-build --local --target=wasm32-wasip1 diff --git a/examples/HelloUI/main.cpp b/examples/HelloUI/main.cpp index d9d4bc9..1a73824 100644 --- a/examples/HelloUI/main.cpp +++ b/examples/HelloUI/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Smoke test for the Tier 1+2+3 UI architecture. Opens a window, draws a // background, a button (Tier 3 component), a slider (Tier 3 component), a // progress bar (Tier 3 component), and a circle that follows the mouse @@ -173,20 +170,14 @@ int main() { 0, 0, 0, 0, }; - // Flush the buffers the GPU is about to read, then composite all three - // categories — background quads, the mouse circle, and the button/label - // text — in ONE fused dispatch (issue #47). The frame runs these passes - // back-to-back with no custom shader interleaved, so DispatchFused loads - // and stores the swapchain image once instead of three times and skips - // the two inter-pass barriers DispatchQuads→Circles→Text would insert. - // Canonical order is quads → circles → images → text; here there are no - // images, so that category is a free no-op. + // Flush + dispatch. The library inserts the inter-dispatch barriers. if (qc > 0) { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM quadsBuf.FlushDevice(cmd, VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); #else quadsBuf.FlushDevice(); #endif + ui.DispatchQuads(cmd, quadsSlot, qc); } if (cc > 0) { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM @@ -194,6 +185,7 @@ int main() { #else circlesBuf.FlushDevice(); #endif + ui.DispatchCircles(cmd, circlesSlot, cc); } if (gc > 0) { #ifndef CRAFTER_GRAPHICS_WINDOW_DOM @@ -201,12 +193,8 @@ int main() { #else glyphsBuf.FlushDevice(); #endif + ui.DispatchText(cmd, glyphsSlot, gc); } - ui.DispatchFused(cmd, - {quadsSlot, qc}, - {circlesSlot, cc}, - {}, // no images this frame - {glyphsSlot, gc}); }); window.FinishInit(); diff --git a/examples/HelloUI/project.cpp b/examples/HelloUI/project.cpp index 9becf9b..409d66d 100644 --- a/examples/HelloUI/project.cpp +++ b/examples/HelloUI/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/HelloWindow/main.cpp b/examples/HelloWindow/main.cpp index a922852..1a8c42a 100644 --- a/examples/HelloWindow/main.cpp +++ b/examples/HelloWindow/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import Crafter.Graphics; import std; using namespace Crafter; diff --git a/examples/HelloWindow/project.cpp b/examples/HelloWindow/project.cpp index 76f9a12..d1a7a5d 100644 --- a/examples/HelloWindow/project.cpp +++ b/examples/HelloWindow/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/InputSystem/main.cpp b/examples/InputSystem/main.cpp index 650f56a..bffd73d 100644 --- a/examples/InputSystem/main.cpp +++ b/examples/InputSystem/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // ===================================================================== // InputSystem — guided tour of Crafter::Input // ===================================================================== diff --git a/examples/InputSystem/project.cpp b/examples/InputSystem/project.cpp index d6db2d3..8c7555f 100644 --- a/examples/InputSystem/project.cpp +++ b/examples/InputSystem/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/LICENSE b/examples/LICENSE deleted file mode 100644 index 7854b67..0000000 --- a/examples/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (C) 2026 Catcrafts® - -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/examples/RTMultiShadow/closesthit.wgsl b/examples/RTMultiShadow/closesthit.wgsl index f37f6c1..98671bc 100644 --- a/examples/RTMultiShadow/closesthit.wgsl +++ b/examples/RTMultiShadow/closesthit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTMultiShadow closest-hit (runs in SHADE). The multi-light counterpart // of RTStress: EVERY light emits its own shadow ray from this single // invocation, so several rays for the same pixel resolve in the next SHADE diff --git a/examples/RTMultiShadow/main.cpp b/examples/RTMultiShadow/main.cpp index f2b7a61..6176650 100644 --- a/examples/RTMultiShadow/main.cpp +++ b/examples/RTMultiShadow/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTMultiShadow — multi-light shadowing through the wavefront RT pipeline // (issue #30). Five pillars on a checkered ground, lit by four colored // point lights; the closest-hit emits one shadow ray PER LIGHT from the diff --git a/examples/RTMultiShadow/miss.wgsl b/examples/RTMultiShadow/miss.wgsl index 88538cf..f3ed3bf 100644 --- a/examples/RTMultiShadow/miss.wgsl +++ b/examples/RTMultiShadow/miss.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTMultiShadow miss (runs in SHADE). Shadow miss → that light is visible // from the surface, so add its pending contribution; up to LIGHT_COUNT of // these resolve for the same pixel in one pass (atomic rtAccumulate, #30). diff --git a/examples/RTMultiShadow/project.cpp b/examples/RTMultiShadow/project.cpp index 378f199..f26d05e 100644 --- a/examples/RTMultiShadow/project.cpp +++ b/examples/RTMultiShadow/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/RTMultiShadow/raygen.wgsl b/examples/RTMultiShadow/raygen.wgsl index cfbf43e..e22e71f 100644 --- a/examples/RTMultiShadow/raygen.wgsl +++ b/examples/RTMultiShadow/raygen.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTMultiShadow raygen (runs in GENERATE). Host-driven pinhole camera at // @group(3) (groups 0..2 are reserved by the wavefront pipeline: // 0 = WfParams, 1 = data heaps, 2 = indirect args). diff --git a/examples/RTMultiShadow/resolve.wgsl b/examples/RTMultiShadow/resolve.wgsl index 6fce5bb..7f32dcf 100644 --- a/examples/RTMultiShadow/resolve.wgsl +++ b/examples/RTMultiShadow/resolve.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTMultiShadow RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the // linear accumulator. Registered as a WebGPURTStage::Resolve shader. fn resolve_main(coord: vec2, hdr: vec4) -> vec4 { diff --git a/examples/RTStress/closesthit.wgsl b/examples/RTStress/closesthit.wgsl index 7888c10..c80f5bb 100644 --- a/examples/RTStress/closesthit.wgsl +++ b/examples/RTStress/closesthit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress closest-hit (runs in SHADE). Computes flat-shaded Lambert from // the hit triangle's geometric normal, accumulates ambient, and — if the // surface faces the sun — emits a shadow ray toward the sun. The shadow diff --git a/examples/RTStress/main.cpp b/examples/RTStress/main.cpp index edd824a..fb64d4e 100644 --- a/examples/RTStress/main.cpp +++ b/examples/RTStress/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress — the standing many-instance wavefront RT benchmark. An // N×N×N grid of a small cube mesh (one BLAS, many TLAS instances), shaded // with primary + shadow rays through the wavefront pipeline. The grid edge diff --git a/examples/RTStress/miss.wgsl b/examples/RTStress/miss.wgsl index 34c0f58..d23a0bc 100644 --- a/examples/RTStress/miss.wgsl +++ b/examples/RTStress/miss.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress miss (runs in SHADE). Primary miss → sky gradient. Shadow miss // → the sun is unoccluded, so add the pending direct contribution. fn miss_main(ray: RayDesc, payload: ptr) { diff --git a/examples/RTStress/project.cpp b/examples/RTStress/project.cpp index 0550928..b1e4f03 100644 --- a/examples/RTStress/project.cpp +++ b/examples/RTStress/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/RTStress/raygen.wgsl b/examples/RTStress/raygen.wgsl index d324e21..def54fc 100644 --- a/examples/RTStress/raygen.wgsl +++ b/examples/RTStress/raygen.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress raygen (runs in GENERATE). Host-driven pinhole camera at // @group(3) (groups 0..2 are reserved by the wavefront pipeline: // 0 = WfParams, 1 = data heaps, 2 = indirect args). diff --git a/examples/RTStress/resolve.wgsl b/examples/RTStress/resolve.wgsl index e585f53..7950c96 100644 --- a/examples/RTStress/resolve.wgsl +++ b/examples/RTStress/resolve.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the linear // accumulator. Registered as a WebGPURTStage::Resolve shader. fn resolve_main(coord: vec2, hdr: vec4) -> vec4 { diff --git a/examples/RTVolume/aabbs.comp.glsl b/examples/RTVolume/aabbs.comp.glsl index 9212ddb..0ad513d 100644 --- a/examples/RTVolume/aabbs.comp.glsl +++ b/examples/RTVolume/aabbs.comp.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_buffer_reference : enable #extension GL_EXT_scalar_block_layout : enable diff --git a/examples/RTVolume/aabbs.comp.wgsl b/examples/RTVolume/aabbs.comp.wgsl index dc95424..aa64f44 100644 --- a/examples/RTVolume/aabbs.comp.wgsl +++ b/examples/RTVolume/aabbs.comp.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Issue #37: GPU producer (WebGPU) for the procedural AABB build input. // Writes the per-box bounding boxes straight into a device storage buffer // that Mesh::BuildProcedural / RefitProcedural then consume by handle, with diff --git a/examples/RTVolume/anyhit.glsl b/examples/RTVolume/anyhit.glsl index 53f4ffc..4ea4882 100644 --- a/examples/RTVolume/anyhit.glsl +++ b/examples/RTVolume/anyhit.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable diff --git a/examples/RTVolume/anyhit.wgsl b/examples/RTVolume/anyhit.wgsl index 104eaee..88fc21d 100644 --- a/examples/RTVolume/anyhit.wgsl +++ b/examples/RTVolume/anyhit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume any-hit shader (runs in TRACE on every candidate sphere hit, // because the geometry is registered non-opaque). Punches a spherical // checkerboard of holes: for half the cells it returns RT_ANYHIT_IGNORE, diff --git a/examples/RTVolume/closesthit.glsl b/examples/RTVolume/closesthit.glsl index 15ecfe4..3ef6d21 100644 --- a/examples/RTVolume/closesthit.glsl +++ b/examples/RTVolume/closesthit.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable diff --git a/examples/RTVolume/closesthit.wgsl b/examples/RTVolume/closesthit.wgsl index f1d12e4..4597101 100644 --- a/examples/RTVolume/closesthit.wgsl +++ b/examples/RTVolume/closesthit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume closest-hit (runs in SHADE). Shades the procedural sphere by // its surface normal with a fixed sun + ambient, tinted per instance. // diff --git a/examples/RTVolume/intersection.glsl b/examples/RTVolume/intersection.glsl index 0af52dd..d46a136 100644 --- a/examples/RTVolume/intersection.glsl +++ b/examples/RTVolume/intersection.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable diff --git a/examples/RTVolume/intersection.wgsl b/examples/RTVolume/intersection.wgsl index 12115cc..1139668 100644 --- a/examples/RTVolume/intersection.wgsl +++ b/examples/RTVolume/intersection.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume intersection shader (runs in TRACE, per AABB the ray enters). // Analytic ray-sphere test: the unit box [-1,1]^3 is treated as the // bounding volume of a sphere of radius 1 centred at the box centre. The diff --git a/examples/RTVolume/main.cpp b/examples/RTVolume/main.cpp index 81e4433..8ea2c13 100644 --- a/examples/RTVolume/main.cpp +++ b/examples/RTVolume/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume — procedural (AABB) ray tracing on both backends. Demonstrates // the two features this example was written to exercise: // diff --git a/examples/RTVolume/miss.glsl b/examples/RTVolume/miss.glsl index 52095e2..3723380 100644 --- a/examples/RTVolume/miss.glsl +++ b/examples/RTVolume/miss.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable diff --git a/examples/RTVolume/miss.wgsl b/examples/RTVolume/miss.wgsl index 48b24a3..533d437 100644 --- a/examples/RTVolume/miss.wgsl +++ b/examples/RTVolume/miss.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume miss (runs in SHADE). Vertical sky gradient — also what shows // through the any-hit cut-out cells. fn miss_main(ray: RayDesc, payload: ptr) { diff --git a/examples/RTVolume/project.cpp b/examples/RTVolume/project.cpp index fcf166d..154ce76 100644 --- a/examples/RTVolume/project.cpp +++ b/examples/RTVolume/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/RTVolume/raygen.glsl b/examples/RTVolume/raygen.glsl index aa13dc4..0825869 100644 --- a/examples/RTVolume/raygen.glsl +++ b/examples/RTVolume/raygen.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable #extension GL_EXT_shader_image_load_formatted : enable diff --git a/examples/RTVolume/raygen.wgsl b/examples/RTVolume/raygen.wgsl index 37b43d7..24fde0d 100644 --- a/examples/RTVolume/raygen.wgsl +++ b/examples/RTVolume/raygen.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume raygen (runs in GENERATE). Host-driven pinhole camera at // @group(3) (groups 0..2 are reserved by the wavefront pipeline: // 0 = WfParams, 1 = data heaps, 2 = indirect args). diff --git a/examples/RTVolume/resolve.wgsl b/examples/RTVolume/resolve.wgsl index 1d49b64..260a033 100644 --- a/examples/RTVolume/resolve.wgsl +++ b/examples/RTVolume/resolve.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTVolume RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the linear // accumulator. fn resolve_main(coord: vec2, hdr: vec4) -> vec4 { diff --git a/examples/RayQueryPick/closesthit.wgsl b/examples/RayQueryPick/closesthit.wgsl index 7888c10..c80f5bb 100644 --- a/examples/RayQueryPick/closesthit.wgsl +++ b/examples/RayQueryPick/closesthit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress closest-hit (runs in SHADE). Computes flat-shaded Lambert from // the hit triangle's geometric normal, accumulates ambient, and — if the // surface faces the sun — emits a shadow ray toward the sun. The shadow diff --git a/examples/RayQueryPick/main.cpp b/examples/RayQueryPick/main.cpp index dd439cf..4cb0606 100644 --- a/examples/RayQueryPick/main.cpp +++ b/examples/RayQueryPick/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RayQueryPick — regression test for the WebGPU software ray-query shim. // // Builds an 8³ = 512-instance TLAS (well below the 8193 threshold where a @@ -192,17 +189,8 @@ int main() { push.origin[2] = origin0 + float(kHitZ) * kSpacing; push.dir[0] = -1.0f; push.dir[1] = 0.0f; push.dir[2] = 0.0f; - // Drive byteCount-bounded prefix readback (issue #133) once the full read - // has verified the hit: re-read ONLY the first 8 bytes (hit + - // instanceCustomIndex) with byteCount=8 after poisoning the host mirror, - // and confirm the two prefix fields land while primitiveIndex (bytes 8..11) - // stays poisoned — i.e. the copy was actually bounded, not full-capacity. - constexpr std::uint32_t kPoison = 0xEEEEEEEEu; - constexpr std::uint32_t kPrefixLen = 2 * sizeof(std::uint32_t); // hit + customIndex - static int frame = 0; static bool dispatched = false; - static bool prefixSent = false; static bool reported = false; EventListener tick(&window.onBeforeUpdate, [&]() { if (reported) return; @@ -211,7 +199,7 @@ int main() { pickShader.Dispatch(&push, sizeof(push), pickHandles, 1, 1, 1); pickBuf.EnqueueReadback(); dispatched = true; - } else if (dispatched && !prefixSent && pickBuf.PollReadback()) { + } else if (dispatched && pickBuf.PollReadback()) { const PickResult& r = pickBuf.value[0]; const bool ok = (r.hit == 1u) && (r.instanceCustomIndex == kExpectedCustomIndex); std::println("[RayQueryPick] result: hit={} customIndex={} prim={} t={}", @@ -222,20 +210,6 @@ int main() { std::println("[RayQueryPick] FAIL — expected hit=1 customIndex={}, got hit={} customIndex={}", kExpectedCustomIndex, r.hit, r.instanceCustomIndex); } - // Now kick off the bounded prefix re-read. Poison the whole mirror - // first; the prefix poll must overwrite only bytes [0,8). - pickBuf.value[0] = { kPoison, kPoison, kPoison, 0.0f }; - pickBuf.EnqueueReadback(/*resetBytes*/ 0, /*byteCount*/ kPrefixLen); - prefixSent = true; - } else if (prefixSent && pickBuf.PollReadback(/*byteCount*/ kPrefixLen)) { - const PickResult& r = pickBuf.value[0]; - const bool prefixOk = (r.hit == 1u) - && (r.instanceCustomIndex == kExpectedCustomIndex) - && (r.primitiveIndex == kPoison); // tail untouched - std::println("[RayQueryPick] prefix readback (byteCount={}): hit={} customIndex={} prim=0x{:08X}", - kPrefixLen, r.hit, r.instanceCustomIndex, r.primitiveIndex); - std::println("[RayQueryPick] {} — byteCount-bounded readback copied only the live prefix", - prefixOk ? "PASS" : "FAIL"); // The render loop runs after main's _Exit, where stdio is never // flushed implicitly — push the verdict out explicitly. std::fflush(stdout); diff --git a/examples/RayQueryPick/miss.wgsl b/examples/RayQueryPick/miss.wgsl index 34c0f58..d23a0bc 100644 --- a/examples/RayQueryPick/miss.wgsl +++ b/examples/RayQueryPick/miss.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress miss (runs in SHADE). Primary miss → sky gradient. Shadow miss // → the sun is unoccluded, so add the pending direct contribution. fn miss_main(ray: RayDesc, payload: ptr) { diff --git a/examples/RayQueryPick/project.cpp b/examples/RayQueryPick/project.cpp index aa156a5..bedc9fd 100644 --- a/examples/RayQueryPick/project.cpp +++ b/examples/RayQueryPick/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/RayQueryPick/raygen.wgsl b/examples/RayQueryPick/raygen.wgsl index d324e21..def54fc 100644 --- a/examples/RayQueryPick/raygen.wgsl +++ b/examples/RayQueryPick/raygen.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress raygen (runs in GENERATE). Host-driven pinhole camera at // @group(3) (groups 0..2 are reserved by the wavefront pipeline: // 0 = WfParams, 1 = data heaps, 2 = indirect args). diff --git a/examples/RayQueryPick/rayquery_pick.wgsl b/examples/RayQueryPick/rayquery_pick.wgsl index a77dd06..b58b4b9 100644 --- a/examples/RayQueryPick/rayquery_pick.wgsl +++ b/examples/RayQueryPick/rayquery_pick.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // rayQuery picking smoke test (WebGPU/DOM software ray-query path). // // Shoots a single, fully-determined ray at a known TLAS instance through diff --git a/examples/RayQueryPick/resolve.wgsl b/examples/RayQueryPick/resolve.wgsl index e585f53..7950c96 100644 --- a/examples/RayQueryPick/resolve.wgsl +++ b/examples/RayQueryPick/resolve.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // RTStress RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the linear // accumulator. Registered as a WebGPURTStage::Resolve shader. fn resolve_main(coord: vec2, hdr: vec4) -> vec4 { diff --git a/examples/Sponza/closesthit.glsl b/examples/Sponza/closesthit.glsl index 1d2de90..2355d9a 100644 --- a/examples/Sponza/closesthit.glsl +++ b/examples/Sponza/closesthit.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable #extension GL_EXT_shader_image_load_formatted : enable diff --git a/examples/Sponza/closesthit.wgsl b/examples/Sponza/closesthit.wgsl index 2b648ee..293f8ba 100644 --- a/examples/Sponza/closesthit.wgsl +++ b/examples/Sponza/closesthit.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Sponza closest-hit (runs in SHADE). In the wavefront model the lighting // + shadow trace that used to live in raygen happens here: gather surface // data, accumulate ambient, and emit a shadow ray toward the sun carrying diff --git a/examples/Sponza/main.cpp b/examples/Sponza/main.cpp index 49ca559..0d4c265 100644 --- a/examples/Sponza/main.cpp +++ b/examples/Sponza/main.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Sponza on Vulkan + WebGPU. Same example source, two backends — picked // by CRAFTER_GRAPHICS_WINDOW_DOM. Both paths: // 1. Load a Sponza .cmesh (positions + indices, optional per-vertex diff --git a/examples/Sponza/miss.glsl b/examples/Sponza/miss.glsl index aa993ad..909d9ca 100644 --- a/examples/Sponza/miss.glsl +++ b/examples/Sponza/miss.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable diff --git a/examples/Sponza/miss.wgsl b/examples/Sponza/miss.wgsl index 9932f18..0bbbf4d 100644 --- a/examples/Sponza/miss.wgsl +++ b/examples/Sponza/miss.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Sponza miss (runs in SHADE). Primary miss → two-stop sky gradient. // Shadow miss → the sun is unoccluded, so add the pending direct term. fn miss_main(ray: RayDesc, payload: ptr) { diff --git a/examples/Sponza/project.cpp b/examples/Sponza/project.cpp index 82cb593..400b0c2 100644 --- a/examples/Sponza/project.cpp +++ b/examples/Sponza/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; diff --git a/examples/Sponza/raygen.glsl b/examples/Sponza/raygen.glsl index 1bee2f0..7eafa4b 100644 --- a/examples/Sponza/raygen.glsl +++ b/examples/Sponza/raygen.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_EXT_ray_tracing : enable #extension GL_EXT_shader_image_load_formatted : enable diff --git a/examples/Sponza/raygen.wgsl b/examples/Sponza/raygen.wgsl index 42f6ac2..6575b34 100644 --- a/examples/Sponza/raygen.wgsl +++ b/examples/Sponza/raygen.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Sponza raygen (runs in GENERATE). Emits the pixel's primary ray; all // shading + the shadow trace now happen in SHADE (closesthit/miss). Camera // state comes from the host each frame via a storage buffer at diff --git a/examples/Sponza/resolve.wgsl b/examples/Sponza/resolve.wgsl index a897140..346659e 100644 --- a/examples/Sponza/resolve.wgsl +++ b/examples/Sponza/resolve.wgsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: MIT -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Sponza RESOLVE-stage tonemap: Reinhard + gamma 2.2 over the linear // accumulator — matches the tonemap the megakernel raygen applied inline. fn resolve_main(coord: vec2, hdr: vec4) -> vec4 { diff --git a/examples/SponzaBench/README.md b/examples/SponzaBench/README.md deleted file mode 100644 index fdf3426..0000000 --- a/examples/SponzaBench/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# SponzaBench — measuring the post-#40 performance work - -This example exists to answer issue **#155**: *starting with #40 a lot of -performance-related issues were merged — what is the net performance gain, -measured in a representative scene (Sponza)?* - -It is a headless benchmark harness built around the **native Vulkan** -Sponza ray-tracing scene. Same asset bundle and camera as -[`examples/Sponza`](../Sponza), but instead of opening an interactive -window it times the work and prints machine-readable `BENCH …` lines, then -exits. - -## What it measures - -* **setup** — process start through the first command submission: asset - decompression, BLAS build per mesh group, the multi-instance TLAS, the - RT pipeline, GPU memory placement and the descriptor writes. This is the - window most of the post-#40 *native* perf work acts on. -* **frames** — a warmup followed by a measured loop calling - `Window::Render()`, reporting per-frame wall-clock stats and throughput. - -Unlike the interactive Sponza example — which is single-material on native -because of the hit-shader dynamic-`descriptor_heap`-index driver fault -(see `examples/Sponza/README.md`) — SponzaBench's closest-hit shades from -barycentric coordinates and samples **no** texture, so it can build the -**full multi-mesh atrium** (25 mesh groups, ~262 k triangles) as one -multi-instance TLAS. The albedo is still decompressed and uploaded during -setup (to keep that path in the measurement) but is not bound. - -## Running - -```bash -cd examples/SponzaBench -crafter-build # native Vulkan -# from the produced bin dir: -VK_LOADER_LAYERS_DISABLE='~all~' \ -CRAFTER_PRESENT_IMMEDIATE=1 \ -BENCH_WARMUP=200 BENCH_FRAMES=2000 BENCH_MESHES=25 ./SponzaBench -``` - -`run-bench.sh ` links possible: take the click, cancel the - // navigation, then Router::PushState. Without it the handler runs - // *and* the browser performs a full page load. It is also mandatory - // on dragover/drop — a drop target does not function at all unless - // the default is cancelled. - // - // Wheel additionally registers with `{ passive: false }` when set, - // or the browser drops the cancellation with a console warning. - // - // AddSubmitListener has no flag: its handler always preventDefaults, - // because a native form POST would navigate away from the wasm app. - std::int32_t AddClickListener(std::function callback, - bool preventDefault = false); + std::int32_t AddClickListener(std::function callback); void RemoveClickListener(std::int32_t id); - std::int32_t AddMouseOverListener(std::function callback, - bool preventDefault = false); + std::int32_t AddMouseOverListener(std::function callback); void RemoveMouseOverListener(std::int32_t id); - std::int32_t AddMouseOutListener(std::function callback, - bool preventDefault = false); + std::int32_t AddMouseOutListener(std::function callback); void RemoveMouseOutListener(std::int32_t id); - std::int32_t AddMouseMoveListener(std::function callback, - bool preventDefault = false); + std::int32_t AddMouseMoveListener(std::function callback); void RemoveMouseMoveListener(std::int32_t id); - std::int32_t AddMouseDownListener(std::function callback, - bool preventDefault = false); + std::int32_t AddMouseDownListener(std::function callback); void RemoveMouseDownListener(std::int32_t id); - std::int32_t AddMouseUpListener(std::function callback, - bool preventDefault = false); + std::int32_t AddMouseUpListener(std::function callback); void RemoveMouseUpListener(std::int32_t id); std::int32_t AddFocusListener(std::function callback); @@ -156,16 +115,13 @@ export namespace Crafter::Dom { std::int32_t AddBlurListener(std::function callback); void RemoveBlurListener(std::int32_t id); - std::int32_t AddKeyDownListener(std::function callback, - bool preventDefault = false); + std::int32_t AddKeyDownListener(std::function callback); void RemoveKeyDownListener(std::int32_t id); - std::int32_t AddKeyUpListener(std::function callback, - bool preventDefault = false); + std::int32_t AddKeyUpListener(std::function callback); void RemoveKeyUpListener(std::int32_t id); - std::int32_t AddKeyPressListener(std::function callback, - bool preventDefault = false); + std::int32_t AddKeyPressListener(std::function callback); void RemoveKeyPressListener(std::int32_t id); std::int32_t AddChangeListener(std::function callback); @@ -183,36 +139,28 @@ export namespace Crafter::Dom { std::int32_t AddScrollListener(std::function callback); void RemoveScrollListener(std::int32_t id); - std::int32_t AddContextMenuListener(std::function callback, - bool preventDefault = false); + std::int32_t AddContextMenuListener(std::function callback); void RemoveContextMenuListener(std::int32_t id); - std::int32_t AddDragStartListener(std::function callback, - bool preventDefault = false); + std::int32_t AddDragStartListener(std::function callback); void RemoveDragStartListener(std::int32_t id); - std::int32_t AddDragEndListener(std::function callback, - bool preventDefault = false); + std::int32_t AddDragEndListener(std::function callback); void RemoveDragEndListener(std::int32_t id); - std::int32_t AddDropListener(std::function callback, - bool preventDefault = false); + std::int32_t AddDropListener(std::function callback); void RemoveDropListener(std::int32_t id); - std::int32_t AddDragOverListener(std::function callback, - bool preventDefault = false); + std::int32_t AddDragOverListener(std::function callback); void RemoveDragOverListener(std::int32_t id); - std::int32_t AddDragEnterListener(std::function callback, - bool preventDefault = false); + std::int32_t AddDragEnterListener(std::function callback); void RemoveDragEnterListener(std::int32_t id); - std::int32_t AddDragLeaveListener(std::function callback, - bool preventDefault = false); + std::int32_t AddDragLeaveListener(std::function callback); void RemoveDragLeaveListener(std::int32_t id); - std::int32_t AddWheelListener(std::function callback, - bool preventDefault = false); + std::int32_t AddWheelListener(std::function callback); void RemoveWheelListener(std::int32_t id); protected: diff --git a/interfaces/Crafter.Graphics-DomEvents.cppm b/interfaces/Crafter.Graphics-DomEvents.cppm index 7dcaf32..cd3c4a5 100644 --- a/interfaces/Crafter.Graphics-DomEvents.cppm +++ b/interfaces/Crafter.Graphics-DomEvents.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // DOM event POD structs delivered by dom-env.js into the per-listener // callback set in :Dom. The set of fields and their semantics mirror the diff --git a/interfaces/Crafter.Graphics-Font.cppm b/interfaces/Crafter.Graphics-Font.cppm index 5132587..14710d9 100644 --- a/interfaces/Crafter.Graphics-Font.cppm +++ b/interfaces/Crafter.Graphics-Font.cppm @@ -1,5 +1,22 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #include "../lib/stb_truetype.h" @@ -55,17 +72,5 @@ namespace Crafter { float LineHeight(float size); float AscentPx(float size); float ScaleForSize(float size); - - // Horizontal advance for `cp` in unscaled font units, cached per Font. - // Stored in font units (not pixels) so it can be rescaled for any - // `size`; Glyph::advance in the FontAtlas is baked at kBaseSize and is - // wrong at other sizes, so it cannot be reused here. ASCII lands in a - // flat array (the common path for caret hit-testing, which rescans the - // whole field per character), everything else in the map. - std::int32_t AdvanceUnits(std::uint32_t cp); - - private: - std::array asciiAdvance_; // -1 = uncached - std::unordered_map advanceUnits_; }; } diff --git a/interfaces/Crafter.Graphics-FontAtlas.cppm b/interfaces/Crafter.Graphics-FontAtlas.cppm index 4f72522..105f011 100644 --- a/interfaces/Crafter.Graphics-FontAtlas.cppm +++ b/interfaces/Crafter.Graphics-FontAtlas.cppm @@ -1,6 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" @@ -87,12 +102,9 @@ export namespace Crafter { std::vector staging; #endif // `dirty` stays the cheap "is there anything to flush?" flag the - // renderer polls each frame; it is the OR of every dirty span Update - // copies. `dirtyRect` is the *whole-atlas* span — used only for the - // one-shot zero-clear in Initialize; per-glyph dirt is tracked tight - // per shelf (Shelf::dirty) instead of inflating one tall union box - // across scattered shelves (#129). `dirty` and the spans are always - // armed and cleared together. + // renderer polls each frame; `dirtyRect` carries the bounds Update + // copies. The two are always set and cleared together (dirty == + // !dirtyRect.Empty()). bool dirty = false; DirtyRect dirtyRect; @@ -119,11 +131,7 @@ export namespace Crafter { } private: - // A shelf packs glyphs left-to-right at a fixed top (`y`). Its `dirty` - // span therefore stays naturally tight: a contiguous X run capped by - // the shelf height — far smaller than a union box spanning every - // shelf a frame happened to touch. - struct Shelf { int y = 0; int height = 0; int cursorX = 0; DirtyRect dirty; }; + struct Shelf { int y = 0; int height = 0; int cursorX = 0; }; std::vector shelves_; int nextShelfY_ = 0; @@ -141,8 +149,6 @@ export namespace Crafter { }; std::unordered_map cache_; - // On success, outShelf is the index into shelves_ of the placed - // glyph, so the caller can mark that shelf's dirty span. - bool ShelfPlace(int w, int h, int& outX, int& outY, int& outShelf); + bool ShelfPlace(int w, int h, int& outX, int& outY); }; } diff --git a/interfaces/Crafter.Graphics-ForwardDeclarations.cppm b/interfaces/Crafter.Graphics-ForwardDeclarations.cppm index 76cd516..3534166 100644 --- a/interfaces/Crafter.Graphics-ForwardDeclarations.cppm +++ b/interfaces/Crafter.Graphics-ForwardDeclarations.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ export module Crafter.Graphics:ForwardDeclarations; import std; diff --git a/interfaces/Crafter.Graphics-Gamepad.cppm b/interfaces/Crafter.Graphics-Gamepad.cppm index 7ff8dd8..6dfe285 100644 --- a/interfaces/Crafter.Graphics-Gamepad.cppm +++ b/interfaces/Crafter.Graphics-Gamepad.cppm @@ -1,6 +1,22 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +Catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ export module Crafter.Graphics:Gamepad; import std; import Crafter.Event; diff --git a/interfaces/Crafter.Graphics-GraphicsTypes.cppm b/interfaces/Crafter.Graphics-GraphicsTypes.cppm index b067313..7baf420 100644 --- a/interfaces/Crafter.Graphics-GraphicsTypes.cppm +++ b/interfaces/Crafter.Graphics-GraphicsTypes.cppm @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® diff --git a/interfaces/Crafter.Graphics-Image2D.cppm b/interfaces/Crafter.Graphics-Image2D.cppm index f9df759..07d09be 100644 --- a/interfaces/Crafter.Graphics-Image2D.cppm +++ b/interfaces/Crafter.Graphics-Image2D.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Image2D — portable 2D image type whose API surface is intentionally // backend-specific via #ifdef. On Vulkan it aliases the existing diff --git a/interfaces/Crafter.Graphics-ImageVulkan.cppm b/interfaces/Crafter.Graphics-ImageVulkan.cppm index 5064c4e..9ba72a6 100644 --- a/interfaces/Crafter.Graphics-ImageVulkan.cppm +++ b/interfaces/Crafter.Graphics-ImageVulkan.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 0215-1301 USA +*/ module; @@ -15,41 +31,6 @@ import :Decompress; import :VulkanBuffer; export namespace Crafter { - // Builds the batched final layout transition for a freshly-generated mip - // chain (mipLevels >= 2). After the blit loop, levels [0, mipLevels-1) sit - // in TRANSFER_SRC_OPTIMAL — each was read as a blit source — while the last - // level is still in TRANSFER_DST_OPTIMAL: it was written by the final blit - // and is never read, so it never needed a DST->SRC barrier of its own. - // Both groups move to the consumer `layout` in one vkCmdPipelineBarrier - // (two VkImageMemoryBarrier entries sharing src=TRANSFER / dst=consumer - // stage), shaving one barrier call off the previous N+1-per-chain count. - // The interleaved per-level barriers stay one-at-a-time — each is mandated - // because the next blit reads the level it transitions. Writes the entries - // into `out` and returns the count (always 2). - std::uint32_t BuildMipChainFinalBarriers(VkImage image, VkImageLayout layout, std::uint32_t mipLevels, std::array& out) { - auto fill = [&](VkImageMemoryBarrier& b, VkImageLayout oldLayout, std::uint32_t baseMip, std::uint32_t count, VkAccessFlags srcAccess) { - b = {}; - b.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; - b.oldLayout = oldLayout; - b.newLayout = layout; - b.image = image; - b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - b.subresourceRange.baseMipLevel = baseMip; - b.subresourceRange.levelCount = count; - b.subresourceRange.baseArrayLayer = 0; - b.subresourceRange.layerCount = 1; - b.srcAccessMask = srcAccess; - b.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - }; - // Levels [0, mipLevels-1): blit sources, currently TRANSFER_SRC. - fill(out[0], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, mipLevels - 1u, VK_ACCESS_TRANSFER_READ_BIT); - // Final level: the last blit's destination, still TRANSFER_DST. - fill(out[1], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels - 1u, 1u, VK_ACCESS_TRANSFER_WRITE_BIT); - return 2; - } - template class ImageVulkan { public: @@ -59,12 +40,9 @@ export namespace Crafter { VkImage image; VkDeviceMemory imageMemory; VulkanBuffer buffer; - // Transient host-visible staging for the compressed Update path. Same - // lifetime contract as Mesh::compressedStaging: the compressed Update - // releases it via DeferredClear() right after recording the decompress, - // so the fence-keyed deletion queue (#101/#102) frees it once that - // submit's frame has cleared instead of pinning it for the image's life. - // Between Updates the handle is null; the next Update's Resize re-creates it. + // Lives until the compressed Update path's cmd buffer completes. + // Same lifetime contract as Mesh::compressedStaging — caller must + // not destroy / re-Update before the submit fence is signaled. VulkanBuffer compressedStaging; VkImageView imageView; VkDescriptorImageInfo descriptor; @@ -74,24 +52,12 @@ export namespace Crafter { // consumer. Defaults to RT — the font atlas overrides it to COMPUTE, // since UI text is rendered by a compute shader, not an RT pipeline. VkPipelineStageFlags consumerStage = VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR; - // Whether this image is continuously re-uploaded from a persistent - // CPU-side staging map. False (the default) marks a static texture - // uploaded once — e.g. Sponza albedo: the first Update's buffer→image - // copy is the only reader of the host-visible staging `buffer`, so it - // is released to the fence-keyed deletion queue (#101/#102) right after - // the copy instead of pinning HOST_VISIBLE / small-BAR memory for the - // image's whole life (issue #114). True keeps `buffer` alive: a streamed - // image (the FontAtlas) re-fills buffer.value on the CPU and re-uploads - // every frame, so the persistent map must survive — its uploads go - // through UpdateRegion, which never releases the staging. - bool streamed = false; - void Create(std::uint16_t width, std::uint16_t height, std::uint8_t mipLevels, VkCommandBuffer cmd, VkFormat format, VkImageCreateFlags flags, VkImageLayout layout, VkPipelineStageFlags consumerStage = VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, bool streamed = false) { + void Create(std::uint16_t width, std::uint16_t height, std::uint8_t mipLevels, VkCommandBuffer cmd, VkFormat format, VkImageCreateFlags flags, VkImageLayout layout, VkPipelineStageFlags consumerStage = VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) { this->width = width; this->height = height; this->mipLevels = mipLevels; this->consumerStage = consumerStage; - this->streamed = streamed; buffer.Create( VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, @@ -194,21 +160,13 @@ export namespace Crafter { blit.dstOffsets[1] = { (int32_t)mipWidth, (int32_t)mipHeight, 1 }; vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); - // The final blit's destination is never read again, so it - // skips the DST->SRC barrier and is taken straight to the - // consumer layout by the batched final transition below. - if (i + 1 < mipLevels) { - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1); - } + TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1); } - std::array finalBarriers; - std::uint32_t finalCount = BuildMipChainFinalBarriers(image, layout, mipLevels, finalBarriers); - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, 0, 0, nullptr, 0, nullptr, finalCount, finalBarriers.data()); + TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); } else { TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); } - ReleaseStaging(); } // Upload only the sub-rectangle [x, x+w) × [y, y+h) of the staging @@ -299,16 +257,6 @@ export namespace Crafter { VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); - // Compressed staging is read only by the decompress recorded above; - // the buffer→image copy below reads `buffer` (the decompress dst), - // never this. Release it to the fence-keyed deletion queue - // (#101/#102) now instead of pinning host-visible memory for the - // image's whole life. The recorded vkCmdDecompressMemoryEXT still - // references compressedStaging.address, so it must outlive this - // submit — which the queue guarantees by retiring the allocation - // only after framesInFlight frames (i.e. after the submit's fence). - compressedStaging.DeferredClear(); - // Continue with the existing buffer→image upload + layout transitions. // We've already inserted the decompress→transfer-read barrier, // so we skip the FlushDevice host-write barrier the regular Update @@ -349,50 +297,21 @@ export namespace Crafter { blit.dstOffsets[0] = { 0, 0, 0 }; blit.dstOffsets[1] = { (int32_t)mipWidth, (int32_t)mipHeight, 1 }; vkCmdBlitImage(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR); - // The final blit's destination is never read again, so it - // skips the DST->SRC barrier and is taken straight to the - // consumer layout by the batched final transition below. - if (i + 1 < mipLevels) { - TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1); - } + TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, i, 1); } - std::array finalBarriers; - std::uint32_t finalCount = BuildMipChainFinalBarriers(image, layout, mipLevels, finalBarriers); - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, 0, 0, nullptr, 0, nullptr, finalCount, finalBarriers.data()); + TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); } else { TransitionImageLayout(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layout, VK_PIPELINE_STAGE_TRANSFER_BIT, consumerStage, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0, mipLevels); } - ReleaseStaging(); } void Destroy() { vkDestroyImageView(Device::device, imageView, nullptr); vkDestroyImage(Device::device, image, nullptr); vkFreeMemory(Device::device, imageMemory, nullptr); - // Free any staging this image still owns: a streamed image's - // persistent `buffer`, or either staging buffer on an image - // destroyed before its first upload released them (issue #114). - // ReleaseStaging()/Update already null released handles, so these - // are no-ops then; Clear() is gated on a live handle so a static - // texture (whose `buffer` was deferred-cleared) can't double-free. - if (buffer.buffer != VK_NULL_HANDLE) buffer.Clear(); - if (compressedStaging.buffer != VK_NULL_HANDLE) compressedStaging.Clear(); } private: - // Release the host-visible staging `buffer` after a one-shot upload, - // unless this is a streamed image (FontAtlas) whose persistent map must - // survive. The recorded buffer→image copy still references buffer.buffer, - // so route through the fence-keyed deletion queue (#101/#102) instead of - // an immediate Clear(): DeferredClear nulls the handle (so re-entry and - // Destroy treat the staging as already gone) while the allocation - // outlives this submit's frame. Issue #114. - void ReleaseStaging() { - if (!streamed) { - buffer.DeferredClear(); - } - } - void TransitionImageLayout(VkCommandBuffer cmd, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask, std::uint32_t mipLevel, std::uint32_t count) { VkImageMemoryBarrier barrier = {}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; diff --git a/interfaces/Crafter.Graphics-Input.cppm b/interfaces/Crafter.Graphics-Input.cppm index a1ea11d..c3b28d1 100644 --- a/interfaces/Crafter.Graphics-Input.cppm +++ b/interfaces/Crafter.Graphics-Input.cppm @@ -1,6 +1,22 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +Catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ export module Crafter.Graphics:Input; import std; import Crafter.Math; diff --git a/interfaces/Crafter.Graphics-InputField.cppm b/interfaces/Crafter.Graphics-InputField.cppm index 3eecfd4..0812858 100644 --- a/interfaces/Crafter.Graphics-InputField.cppm +++ b/interfaces/Crafter.Graphics-InputField.cppm @@ -1,6 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; export module Crafter.Graphics:InputField; @@ -43,20 +58,6 @@ export namespace Crafter { InputFieldType type = InputFieldType::Text; bool focused = false; std::size_t cursorPos = 0; // byte offset into `value` - - // Caret-x memo (issue #128). For a focused field DrawInputField measures - // the cursor prefix via Font::GetLineWidth every frame, but only the - // blink (caretVisible) changes frame-to-frame — value/cursorPos/fontSize - // are stable across the vast majority of frames. Cache the measured - // prefix WIDTH (the GetLineWidth result, in px) keyed on the inputs that - // determine it: the prefix bytes and fontSize. The final caretX adds the - // layout-dependent textX each frame, so it is intentionally NOT cached — - // a field that moves on screen with an unchanged value would otherwise - // get a stale caret. `mutable` so the `const InputField&` draw fn can - // refresh it; these fields are internal and need no user initialisation. - mutable std::string caretCachePrefix_; - mutable float caretCacheFontSize_ = -1.0f; // <0 = unset - mutable float caretCacheWidth_ = 0.0f; }; // Returns true if `s` is a valid candidate for the given type, including diff --git a/interfaces/Crafter.Graphics-Keys.cppm b/interfaces/Crafter.Graphics-Keys.cppm index 1e54e90..c21c767 100644 --- a/interfaces/Crafter.Graphics-Keys.cppm +++ b/interfaces/Crafter.Graphics-Keys.cppm @@ -1,6 +1,22 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +Catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND #include diff --git a/interfaces/Crafter.Graphics-Mesh.cppm b/interfaces/Crafter.Graphics-Mesh.cppm index 5fd8875..c04f819 100644 --- a/interfaces/Crafter.Graphics-Mesh.cppm +++ b/interfaces/Crafter.Graphics-Mesh.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; @@ -55,25 +71,18 @@ export namespace Crafter { public: VulkanBuffer scratchBuffer; VulkanBuffer blasBuffer; - // Non-mapped (device-local) RT geometry: VulkanBuffer::UploadDeviceLocal - // places these in device memory and picks direct-map vs staged-copy per - // the #89 upload strategy, so they live in VRAM for hit-shader fetch and - // BLAS-build/refit reads instead of being read from system RAM over PCIe - // every trace (#73). The compressed Build path allocates them pure - // DEVICE_LOCAL and lets the GPU decompressor fill them directly. - VulkanBuffer, false> vertexBuffer; - VulkanBuffer indexBuffer; + VulkanBuffer, true> vertexBuffer; + VulkanBuffer indexBuffer; // AABB build input for the procedural path (BuildProcedural). // Lifetime contract matches vertexBuffer/indexBuffer: must stay // alive until the build submitted on `cmd` completes. - VulkanBuffer aabbBuffer; - // Transient host-visible staging for the compressed Build path. Kept as - // a member only so the recorded vkCmdDecompressMemoryEXT has a stable - // address to reference; the compressed Build releases it via - // DeferredClear() right after recording the decompress, so it is freed - // by the fence-keyed deletion queue (#101/#102) once that submit's frame - // has cleared — no longer pinned for the mesh's life. Between Builds the - // handle is null; the next Build's Resize re-creates it. + VulkanBuffer aabbBuffer; + // Lives until the cmd buffer issued by the compressed Build path + // completes execution. Kept as a member so the recorded + // vkCmdDecompressMemoryEXT references valid memory until the queue + // submit signals — caller must not re-Build or destroy the Mesh + // before that submit's fence is signaled (same contract as the + // existing uncompressed path). VulkanBuffer compressedStaging; VkAccelerationStructureGeometryTrianglesDataKHR blasData; VkAccelerationStructureGeometryKHR blas; @@ -139,31 +148,12 @@ export namespace Crafter { // a hardware UPDATE-mode build — much cheaper than a rebuild, and the // BLAS handle / blasAddr are preserved, so TLAS instances referencing // it stay valid. A hardware update may only move vertex positions, - // not change connectivity, so on the UPDATE path only the vertex - // buffer is re-uploaded; the index buffer is left untouched (its - // contents are immutable for an UPDATE). The `indicies` span is read - // only for its count — a bitwise-different but topologically-equivalent - // index array is ignored on refit. Without allowUpdate (or if the - // counts changed) it falls back to a full rebuild, which does re-upload - // both buffers. Lifetime contract matches Build: the spans need only + // not change connectivity; the indices are re-uploaded for symmetry + // with the rebuild path but must describe the same topology. Without + // allowUpdate (or if the counts changed) it falls back to a full + // rebuild. Lifetime contract matches Build: the spans need only // outlive this call. Call this per frame to track a deforming mesh. void Refit(std::span> verticies, std::span indicies, VkCommandBuffer cmd); - // Dirty-range refit: same as Refit above, but only the contiguous block - // of vertices in [dirtyVertexOffset, dirtyVertexOffset + dirtyVertexCount) - // actually moved, so on the in-place UPDATE path only that sub-range is - // re-uploaded — host-write + flush + barrier (direct) or re-stage + copy - // (staged) scale with the moved vertices, not the whole array (#119). Use - // this for a deforming mesh that nudges a small, known window each frame. - // `verticies` / `indicies` are still the *full* arrays (same contract and - // topology rule as the full-span Refit — the index buffer is read only - // for its count on the UPDATE path); the dirty window simply tells the - // upload which slice changed, and is clamped to the array bounds. When an - // in-place UPDATE is not possible (allowUpdate was not set, or the counts - // changed) this falls back to the full-span Refit, which re-uploads - // everything from `verticies` / `indicies` — so passing the full arrays - // keeps that fallback correct. The AS handle / blasAddr are preserved on - // the UPDATE path exactly as in the full-span Refit. - void Refit(std::span> verticies, std::span indicies, std::uint32_t dirtyVertexOffset, std::uint32_t dirtyVertexCount, VkCommandBuffer cmd); // Procedural analog of Refit: new object-space boxes, same count. void RefitProcedural(std::span aabbs, VkCommandBuffer cmd); // Zero-copy procedural refit: the device-buffer counterpart of @@ -307,17 +297,6 @@ export namespace Crafter { void Refit(std::span> vertices, std::span indices, WebGPUCommandEncoderRef cmd = 0); - // Dirty-range refit (#119). The software path has no hardware AS to - // update a sub-range of — it rebuilds the host BVH over the full - // geometry regardless — so the dirty window is ignored here and this - // behaves exactly like the full-span Refit above (a fresh build that - // re-publishes blasAddr). The overload exists so portable deforming-mesh - // code that passes a dirty window compiles and stays correct on WebGPU. - void Refit(std::span> vertices, - std::span indices, - std::uint32_t dirtyVertexOffset, - std::uint32_t dirtyVertexCount, - WebGPUCommandEncoderRef cmd = 0); void RefitProcedural(std::span aabbs, WebGPUCommandEncoderRef cmd = 0); // Zero-copy procedural refit: re-copy the boxes from the device buffer diff --git a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm index 902bcbb..c0374ec 100644 --- a/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm +++ b/interfaces/Crafter.Graphics-PipelineRTVulkan.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM @@ -66,7 +82,7 @@ export namespace Crafter { .layout = VK_NULL_HANDLE }; - Device::CheckVkResult(Device::vkCreateRayTracingPipelinesKHR(Device::device, {}, Device::pipelineCache, 1, &rtPipelineInfo, nullptr, &pipeline)); + Device::CheckVkResult(Device::vkCreateRayTracingPipelinesKHR(Device::device, {}, {}, 1, &rtPipelineInfo, nullptr, &pipeline)); std::size_t dataSize = Device::rayTracingProperties.shaderGroupHandleSize * rtPipelineInfo.groupCount; shaderHandles.resize(dataSize); @@ -87,30 +103,7 @@ export namespace Crafter { hitRegion.size = hitGroups.size() * sbtStride; std::size_t bufferSize = hitRegion.deviceAddress + hitRegion.size; - // The SBT is written once here (the memcpys below) and read by the - // GPU on every vkCmdTraceRaysKHR for the pipeline's lifetime — the - // textbook write-once/read-many buffer (issue #72). Get it into - // device-local memory so trace dispatches read raygen/miss/hit - // records out of VRAM instead of over PCIe from system RAM. - // - // Route the placement through #89: when a DEVICE_LOCAL|HOST_VISIBLE - // type exists (ReBAR/UMA, or a BAR window — the SBT is tiny so the - // window budget is never the constraint), prefer DEVICE_LOCAL on top - // of the required HOST_VISIBLE so the allocation lands in device - // memory we can still map. The one-time memcpy goes write-combined - // over PCIe (write-only, sequential — ideal); GPU reads then hit - // local VRAM. When no combined type exists at all (no spec - // guarantee), PreferDirectDeviceWrite returns false and the preferred - // hint is dropped, so GetMemoryType falls back to plain HOST_VISIBLE - // (current behaviour, the per-trace PCIe read) — the cheaper fallback - // the issue blesses while staging isn't wired here. - // - // Either way the buffer stays mapped, and the FlushDevice below gates - // its flush on the *chosen* memory type's flags (issue #60), so a - // direct-write type lacking HOST_COHERENT is still flushed correctly. - VkMemoryPropertyFlags sbtPreferred = Device::PreferDirectDeviceWrite(bufferSize) - ? VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT : 0; - sbtBuffer.Create(VK_BUFFER_USAGE_2_SHADER_BINDING_TABLE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferSize, sbtPreferred); + sbtBuffer.Create(VK_BUFFER_USAGE_2_SHADER_BINDING_TABLE_BIT_KHR | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, bufferSize); std::uint8_t* offset = sbtBuffer.value; std::uint8_t* handleOffset = shaderHandles.data(); diff --git a/interfaces/Crafter.Graphics-PipelineRTWebGPU.cppm b/interfaces/Crafter.Graphics-PipelineRTWebGPU.cppm index f309f13..a286994 100644 --- a/interfaces/Crafter.Graphics-PipelineRTWebGPU.cppm +++ b/interfaces/Crafter.Graphics-PipelineRTWebGPU.cppm @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® diff --git a/interfaces/Crafter.Graphics-PlainComputeShader.cppm b/interfaces/Crafter.Graphics-PlainComputeShader.cppm index 2e9d90d..e8957f3 100644 --- a/interfaces/Crafter.Graphics-PlainComputeShader.cppm +++ b/interfaces/Crafter.Graphics-PlainComputeShader.cppm @@ -1,5 +1,12 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; +*/ // Standalone compute pipeline. Dispatches at any point in the frame // (inside or outside the UI render pass) via the JS bridge's diff --git a/interfaces/Crafter.Graphics-RT.cppm b/interfaces/Crafter.Graphics-RT.cppm index 2ec01e8..8ae8fdd 100644 --- a/interfaces/Crafter.Graphics-RT.cppm +++ b/interfaces/Crafter.Graphics-RT.cppm @@ -1,5 +1,12 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; +*/ // Portable RT types & constants. // diff --git a/interfaces/Crafter.Graphics-RTPass.cppm b/interfaces/Crafter.Graphics-RTPass.cppm index 8f9ea3c..37d9bdd 100644 --- a/interfaces/Crafter.Graphics-RTPass.cppm +++ b/interfaces/Crafter.Graphics-RTPass.cppm @@ -1,6 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" @@ -20,13 +35,6 @@ export namespace Crafter { RTPass(PipelineRTVulkan* p) : pipeline(p) {} - // An RT pass writes the swapchain image from the ray-tracing pipeline, - // so the frame loop's barriers must wait on RAY_TRACING_SHADER — using - // the compute stage here would under-synchronise and corrupt the image. - VkPipelineStageFlags SwapchainStage() const override { - return VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR; - } - void Record(VkCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override { vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, pipeline->pipeline); // NVIDIA descriptor-heap AS-read workaround (issue #15 / #7): feed diff --git a/interfaces/Crafter.Graphics-RenderPass.cppm b/interfaces/Crafter.Graphics-RenderPass.cppm index 4c44d6c..e49bc5c 100644 --- a/interfaces/Crafter.Graphics-RenderPass.cppm +++ b/interfaces/Crafter.Graphics-RenderPass.cppm @@ -1,10 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net -module; -#ifndef CRAFTER_GRAPHICS_WINDOW_DOM -#include "vulkan/vulkan.h" -#endif // !CRAFTER_GRAPHICS_WINDOW_DOM +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ export module Crafter.Graphics:RenderPass; import std; import :GraphicsTypes; @@ -12,94 +23,8 @@ import :GraphicsTypes; export namespace Crafter { struct Window; -#ifndef CRAFTER_GRAPHICS_WINDOW_DOM - // Conservative union of every pipeline stage that can write the swapchain - // storage image: a compute pass via COMPUTE_SHADER, a ray-tracing pass via - // RAY_TRACING_SHADER, plus TRANSFER for any blit/copy writer. Used as the - // RenderPass default (a pass that doesn't narrow its own stage) and as the - // fallback when there are no passes to derive a real union from. Far tighter - // than ALL_COMMANDS, but never under-synchronises a polymorphic pass. - inline constexpr VkPipelineStageFlags kSwapchainWriterStages = - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT - | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR - | VK_PIPELINE_STAGE_TRANSFER_BIT; -#endif // !CRAFTER_GRAPHICS_WINDOW_DOM - struct RenderPass { virtual void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) = 0; virtual ~RenderPass() = default; -#ifndef CRAFTER_GRAPHICS_WINDOW_DOM - // Pipeline stage at which this pass reads and writes the swapchain - // storage image. The frame loop uses this to scope the inter-pass and - // frame-edge barriers to the stages that actually touch the image - // instead of ALL_COMMANDS. A subclass MUST narrow this to its real - // stage (RTPass -> RAY_TRACING_SHADER, a compute pass -> COMPUTE_SHADER): - // the default is the conservative writer union so an un-overridden pass - // can never be under-synchronised, only over-synchronised. - virtual VkPipelineStageFlags SwapchainStage() const { return kSwapchainWriterStages; } -#endif // !CRAFTER_GRAPHICS_WINDOW_DOM }; - -#ifndef CRAFTER_GRAPHICS_WINDOW_DOM - // Union of the swapchain-access stages declared by `passes` — the "real" - // per-pass stage union the frame-edge barriers (acquire dst / present src) - // narrow to: an all-compute frame yields COMPUTE_SHADER only, a frame with - // any RT pass folds in RAY_TRACING_SHADER, and so on. Falls back to the - // conservative writer union when there are no passes (the image is still - // transitioned, just never written), so the barrier is always well-formed. - inline VkPipelineStageFlags SwapchainStageUnion(std::span passes) { - if (passes.empty()) return kSwapchainWriterStages; - VkPipelineStageFlags stages = 0; - for (RenderPass* pass : passes) stages |= pass->SwapchainStage(); - return stages; - } - - // The access mask matching a swapchain-writer stage union, for the - // frame-edge barriers (acquire dst / present src). A barrier's access mask - // must only contain access flags supported by its accompanying stage mask - // (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820): TRANSFER_WRITE is - // not a valid access for COMPUTE/RAY_TRACING stages, so hardcoding both - // SHADER_WRITE and TRANSFER_WRITE fires the VUID every frame on an - // all-compute frame. The swapchain image is written as a storage image by - // compute/RT passes (SHADER_WRITE) and only via TRANSFER_WRITE when a - // transfer-stage pass is present, so derive the access bits from the same - // per-pass stage union the barrier's stage mask uses instead of hardcoding. - inline VkAccessFlags SwapchainWriterAccess(VkPipelineStageFlags stages) { - VkAccessFlags access = 0; - if (stages & (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT - | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR)) - access |= VK_ACCESS_SHADER_WRITE_BIT; - if (stages & VK_PIPELINE_STAGE_TRANSFER_BIT) - access |= VK_ACCESS_TRANSFER_WRITE_BIT; - return access; - } - - // The inter-pass swapchain barrier (replacing the old queue-wide - // VkMemoryBarrier at the #115 location). Scoped to the swapchain image's - // single colour subresource so only that image's shader caches round-trip — - // unrelated buffers/images stay resident — exactly as the intra-pass UI - // barrier already does. The image is bound as a storage image and stays in - // VK_IMAGE_LAYOUT_GENERAL, so this is a pure memory dependency (no layout - // transition). The execution scope (stage masks) is supplied per pass pair - // by the caller via vkCmdPipelineBarrier. - inline VkImageMemoryBarrier BuildSwapchainInterPassBarrier(VkImage image) { - return { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT, - .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, - .oldLayout = VK_IMAGE_LAYOUT_GENERAL, - .newLayout = VK_IMAGE_LAYOUT_GENERAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = image, - .subresourceRange = { - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1, - }, - }; - } -#endif // !CRAFTER_GRAPHICS_WINDOW_DOM } diff --git a/interfaces/Crafter.Graphics-RenderingElement3D.cppm b/interfaces/Crafter.Graphics-RenderingElement3D.cppm index f350aa3..cf88d24 100644 --- a/interfaces/Crafter.Graphics-RenderingElement3D.cppm +++ b/interfaces/Crafter.Graphics-RenderingElement3D.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM @@ -42,17 +58,6 @@ export namespace Crafter { // use flags identical to the originating build, so a change in the // requested preference forces a full rebuild rather than a refit. VkBuildAccelerationStructureFlagsKHR builtFlags = 0; - // Per-slot record of the RenderingElement3D::hostDataVersion that - // BuildTLAS last copied into this frame's instanceBuffer/metadataBuffer - // — parallel to them, sized to the live instance count. The per-frame - // copy re-uploads (and flushes) only the slots whose element advanced - // past the recorded version; an element left at version 0 ("untracked") - // reads dirty every frame, so callers that never opt in keep the - // pre-#118 copy-every-frame behaviour. Reset to all-zero on every - // topology change, since a rebuild reshuffles which element occupies - // each slot (and may have reallocated the buffers). See - // RenderingElement3D::hostDataVersion. - std::vector uploadedVersion; }; class RenderingElement3D { @@ -77,43 +82,6 @@ export namespace Crafter { // already live on the GPU). bool transformOwnedByGpu = false; - // Monotonic version of this element's host-authored TLAS data — the - // instance fields the CPU writes (everything except a GPU-owned - // transform; see transformOwnedByGpu) plus userMetadata. BuildTLAS - // records, per frame, the version it last copied into each buffer slot - // (TlasWithBuffer::uploadedVersion) and re-copies a slot only when its - // element has advanced past the recorded version — so a TLAS dominated - // by instances whose host fields are set once and then left alone (the - // millions-of-GPU-driven-bodies target) pays no per-frame host copy or - // flush after the first upload. - // - // 0 is the "untracked" sentinel: an element left at 0 is copied every - // frame exactly as before this optimization, so code that mutates - // instance/userMetadata without opting in stays correct. Opt in by - // calling MarkHostDataDirty after every host-data change — the standard - // dirty-flag contract (mark on change; the upload clears it until the - // next change). The GPU-owned transform is exempt: BuildTLAS never - // host-copies it, so changing it needs no mark. - std::uint64_t hostDataVersion = 0; - - // Stamp this element's host data with a fresh global version so the next - // BuildTLAS of each frame re-uploads (and flushes) its slot. Call after - // changing any host-authored instance field or userMetadata. See - // hostDataVersion. - void MarkHostDataDirty() { hostDataVersion = ++hostDataVersionCounter; } - - // Source of globally-unique, monotonically-increasing host-data - // versions. Global rather than per-element so a version value names a - // unique (element, edit) pair: a per-slot recorded version then equals - // the slot's current occupant only when that exact element's exact edit - // was the last thing written there. That uniqueness is what makes the - // relocation cases — swap-and-pop in Remove, or a remove+add that nets - // the same instance count and so takes the refit path — fall out - // correctly without tracking element identity: a relocated element's - // version never collides with the (different) element's version recorded - // for that slot. 64-bit, so it does not wrap in practice. - inline static std::uint64_t hostDataVersionCounter = 0; - static std::vector elements; inline static TlasWithBuffer tlases[Window::numFrames]; // Build (or in-place refit) the TLAS for frame `index`. `preference` @@ -213,15 +181,6 @@ export namespace Crafter { // element's instanceBuffer slot directly — BuildTLAS preserves it. bool transformOwnedByGpu = false; - // API-symmetric with the Vulkan side so portable code that opts its - // instances into host-data dirty tracking compiles unchanged. The - // WebGPU BuildTLAS re-uploads the whole CPU mirror every build (the - // counts this path targets are small), so the version is not consulted - // here — it exists purely for cross-backend source compatibility. - std::uint64_t hostDataVersion = 0; - void MarkHostDataDirty() { hostDataVersion = ++hostDataVersionCounter; } - inline static std::uint64_t hostDataVersionCounter = 0; - static std::vector elements; inline static TlasWithBuffer tlases[Window::numFrames]; diff --git a/interfaces/Crafter.Graphics-Router.cppm b/interfaces/Crafter.Graphics-Router.cppm index 79d02f7..c0add03 100644 --- a/interfaces/Crafter.Graphics-Router.cppm +++ b/interfaces/Crafter.Graphics-Router.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // History / SPA routing. Thin C++ wrapper over the browser's // `history.pushState` / `popstate` and `window.location.pathname` — @@ -17,62 +33,18 @@ export namespace Crafter::Router { // Push a new history entry. `data` is a JSON string serialized by // the caller — the browser stores it on the entry but the popstate // listener in V1 receives no payload (matches CppDOM's surface). - // `url` is browser-relative, e.g. "/blog/post-1". Cannot leave the - // origin; use Navigate for that. + // `url` is browser-relative, e.g. "/blog/post-1". void PushState(std::string_view data, std::string_view title, std::string_view url); - // As PushState, but rewrites the current entry instead of adding one. - // For URL changes that shouldn't become their own Back destination: - // filter/sort state, canonicalising a sloppy incoming URL. - void ReplaceState(std::string_view data, std::string_view title, std::string_view url); - // Subscribe to the browser's `popstate` event (back/forward button, // programmatic history.go). Returns an opaque id usable with // `RemovePopStateListener`. Multiple subscribers OK. - // - // The callback still receives no payload — re-read the location with - // the accessors below rather than relying on the pushState `data`. std::int32_t AddPopStateListener(std::function callback); void RemovePopStateListener(std::int32_t id); - // Location accessors. Each allocates a fresh string per call — cache - // the result if used in a hot path. - // - // GetSearch / GetHash include their leading '?' / '#', so an absent - // query and a bare "?" stay distinguishable and the result can be - // concatenated straight back onto a path. - std::string GetPath(); // window.location.pathname - std::string GetSearch(); // window.location.search, e.g. "?page=2&sort=new" - std::string GetHash(); // window.location.hash, e.g. "#reviews" - std::string GetHref(); // window.location.href, absolute - - // Full-page navigation, cross-origin allowed. PushState deliberately - // cannot leave the origin, so this is the only way to hand the user off - // to an external URL (a hosted payment page, an OAuth consent screen). - // `replace == true` drops the current page from history, so Back won't - // return to a stale page. - void Navigate(std::string_view url, bool replace = false); - - // ─── Query-string helpers ───────────────────────────────────────── - // - // Pure string functions — no JS round-trip, safe to call on any - // string, not just the live location. Provided here so callers don't - // each hand-roll percent decoding; `+` vs `%20` is the usual bug. - - // Split a query string into decoded key/value pairs, in source order. - // Accepts an optional leading '?'. Duplicate keys are preserved as - // separate entries (?tag=a&tag=b yields two). A valueless key - // ("?debug") yields an empty value. Empty segments are skipped, so - // "a=1&&b=2" and a trailing '&' are tolerated. - std::vector> ParseQuery(std::string_view query); - - // First value for `key`, or nullopt if absent. Distinguishes "missing" - // from "present but empty" — which ?debug and ?q= need. - std::optional QueryGet(std::string_view query, std::string_view key); - - // Percent-decode a single component, treating '+' as a space per - // application/x-www-form-urlencoded. A malformed escape is passed - // through literally rather than dropped. - std::string PercentDecode(std::string_view in); + // Current `window.location.pathname` as a freshly-allocated string. + // Allocates per call — cache the result in the caller if used in + // hot paths. + std::string GetPath(); } #endif // CRAFTER_GRAPHICS_WINDOW_DOM diff --git a/interfaces/Crafter.Graphics-SamplerVulkan.cppm b/interfaces/Crafter.Graphics-SamplerVulkan.cppm index d011568..76e9445 100644 --- a/interfaces/Crafter.Graphics-SamplerVulkan.cppm +++ b/interfaces/Crafter.Graphics-SamplerVulkan.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; diff --git a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm index f922544..aad6b3f 100644 --- a/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderBindingTableVulkan.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM diff --git a/interfaces/Crafter.Graphics-ShaderBindingTableWebGPU.cppm b/interfaces/Crafter.Graphics-ShaderBindingTableWebGPU.cppm index 07563b1..c161879 100644 --- a/interfaces/Crafter.Graphics-ShaderBindingTableWebGPU.cppm +++ b/interfaces/Crafter.Graphics-ShaderBindingTableWebGPU.cppm @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® diff --git a/interfaces/Crafter.Graphics-ShaderVulkan.cppm b/interfaces/Crafter.Graphics-ShaderVulkan.cppm index 0af5520..2dc2d79 100644 --- a/interfaces/Crafter.Graphics-ShaderVulkan.cppm +++ b/interfaces/Crafter.Graphics-ShaderVulkan.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM diff --git a/interfaces/Crafter.Graphics-Types.cppm b/interfaces/Crafter.Graphics-Types.cppm index a7d5eeb..4b76c1f 100644 --- a/interfaces/Crafter.Graphics-Types.cppm +++ b/interfaces/Crafter.Graphics-Types.cppm @@ -1,6 +1,22 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +Catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; // DOM builds drop the Vulkan header — DescriptorBinding (the only // Vk-typed entity here) compiles out under the same guard below. diff --git a/interfaces/Crafter.Graphics-UI.cppm b/interfaces/Crafter.Graphics-UI.cppm index 9675f73..b5760f9 100644 --- a/interfaces/Crafter.Graphics-UI.cppm +++ b/interfaces/Crafter.Graphics-UI.cppm @@ -1,6 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM #include "vulkan/vulkan.h" @@ -44,45 +59,6 @@ export namespace Crafter { }; static_assert(sizeof(UIDispatchHeader) == 48); - // Reserved `flags` bit (mirrors shaders/ui-shared.glsl::UI_FLAG_CLIP). - // FillHeader sets it when the clip rect is narrower than the surface so - // the standard shaders can skip the per-pixel clip compares otherwise. - // The remaining low bits stay free for user-defined feature flags. - inline constexpr std::uint32_t kUIFlagClip = 0x80000000u; - - // ─── fused-dispatch push-constant header (issue #47) ──────────────── - // Mirrors the PC block in shaders/ui-fused.comp.glsl byte-for-byte: every - // member is vec4-aligned, no padding holes, exactly 128 bytes (the - // guaranteed push-constant minimum). This is a SEPARATE contract — the - // per-element Dispatch* path and the frozen 48-byte UIDispatchHeader are - // untouched. DispatchFused builds this; users never fill it directly. - struct UIFusedHeader { - std::uint32_t itemBuffers[4]; // heap slots: quads, circles, images, text - std::uint32_t itemCounts[4]; // item counts: quads, circles, images, text - std::uint32_t outImage; // swapchain image heap slot - std::uint32_t fontTexture; // font-atlas image slot (text phase) - std::uint32_t fontSampler; // font-atlas sampler slot (text phase) - std::uint32_t flags; // per-category clip-active bits (0x1 quads .. 0x8 text) - std::uint32_t surfaceWidth; - std::uint32_t surfaceHeight; - std::uint32_t frameIdx; - std::uint32_t _pad; // reserved — keep zeroed - float clipQuads[4]; // (x, y, w, h) clip per category - float clipCircles[4]; - float clipImages[4]; - float clipText[4]; - }; - static_assert(sizeof(UIFusedHeader) == 128); - - // One category's input to UIRenderer::DispatchFused. A zero itemCount - // makes that category a free no-op (its kernel loop is zero-trip). - // clipRectPx defaults to "no clip", matching the Dispatch* default. - struct FusedBatch { - std::uint32_t bufferSlot = 0; - std::uint32_t itemCount = 0; - std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}; - }; - // ─── standard item PODs (match GLSL std430) ───────────────────────── struct QuadItem { float x, y, w, h; @@ -167,7 +143,6 @@ export namespace Crafter { GraphicsComputeShader drawCircles; GraphicsComputeShader drawImages; GraphicsComputeShader drawText; - GraphicsComputeShader drawFused; FontAtlas* fontAtlas = nullptr; @@ -181,37 +156,15 @@ export namespace Crafter { std::filesystem::path quadsSpv = "ui-quads.comp.spv", std::filesystem::path circlesSpv = "ui-circles.comp.spv", std::filesystem::path imagesSpv = "ui-images.comp.spv", - std::filesystem::path textSpv = "ui-text.comp.spv", - std::filesystem::path fusedSpv = "ui-fused.comp.spv"); + std::filesystem::path textSpv = "ui-text.comp.spv"); void Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) override; -#ifndef CRAFTER_GRAPHICS_WINDOW_DOM - // A UI pass draws into the swapchain image entirely from compute - // shaders (see Dispatch), so the frame loop's inter-pass / frame-edge - // barriers only need to synchronise the COMPUTE_SHADER stage for it. - VkPipelineStageFlags SwapchainStage() const override { - return VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; - } -#endif // !CRAFTER_GRAPHICS_WINDOW_DOM - UIDispatchHeader FillHeader(std::uint32_t itemBufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}, std::uint32_t flags = 0) const noexcept; - // Builds the header `flags` word: the caller's user flags with the - // reserved kUIFlagClip bit set iff `clipRectPx` does not already cover - // the whole surface. When the clip rect spans the surface, the four - // per-pixel clip compares in uiResolveScreenPixel can never reject an - // in-surface pixel, so the bit stays clear and the shaders skip them. - // Static + dimension-parameterised so the decision is unit-testable - // without a live Window; FillHeader feeds it the current window size. - static std::uint32_t ClipFlags(std::array clipRectPx, - std::uint32_t surfaceWidth, - std::uint32_t surfaceHeight, - std::uint32_t flags) noexcept; - void DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}); void DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, @@ -233,32 +186,6 @@ export namespace Crafter { void DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot, std::uint32_t itemCount, std::array clipRectPx = {0.0f, 0.0f, 1e9f, 1e9f}); - // Fused UI dispatch (issue #47). Composites up to four standard - // categories — quads → circles → images → text, canonical - // back-to-front order — in ONE compute dispatch that loads the - // destination image once and stores once, eliminating the per-pass - // load+store and the inter-pass memory barriers a run of consecutive - // Dispatch* calls would pay. The win materialises at >= 2 non-empty - // categories; an empty category (itemCount 0) is a free no-op. - // - // Additive: the per-element Dispatch* calls and UIDispatchHeader are - // untouched. Keep using those for single-category or fine-grained - // work. To interleave a custom ui.Dispatch() between standard passes, - // bracket it with two DispatchFused calls — the dispatch boundary is - // the explicit, app-declared flush point. - // - // The text category uses the registered font atlas (set fontAtlas - // before Initialize); passing a non-empty text batch without one - // throws, exactly like DispatchText. On the WebGPU backend this falls - // back to issuing the per-element Dispatch* calls in canonical order - // (one texture/sampler per dispatch there — see DispatchImages), - // so the result matches; the load/store fusion is Vulkan-only. - void DispatchFused(GraphicsCommandBuffer cmd, - const FusedBatch& quads, - const FusedBatch& circles, - const FusedBatch& images, - const FusedBatch& text); - // Generic dispatch for user-authored shaders. On Vulkan, `shader` is // a SPIR-V compute pipeline (bindless via VK_EXT_descriptor_heap, so // any resource indices baked into push data resolve through the @@ -310,17 +237,6 @@ export namespace Crafter { // reallocated at the same address would otherwise alias stale runs. void InvalidateFont(const Font& font) noexcept; - // Shaped-run cache introspection (issue #123). Hits/misses are - // byte-identical and a re-shape of an already-rasterised string leaves - // the atlas clean, so the LRU eviction policy isn't observable through - // output alone — these let tests assert that the cache stays bounded - // and that the hot set survives an overflow. IsShapedRunCached is a - // pure query: it does not bump recency. - std::size_t ShapedRunCacheSize() const noexcept { return shapedRuns_.size(); } - bool IsShapedRunCached(const Font& font, float pxSize, - std::array color, - std::string_view utf8) const; - std::uint16_t FontAtlasImageSlot() const noexcept { return fontAtlasImageSlot_; } std::uint16_t FontAtlasSamplerSlot() const noexcept { return fontAtlasSamplerSlot_; } @@ -335,86 +251,35 @@ export namespace Crafter { // only needs a translate by (x + alignShift, baselineY). The full // string is part of the key (compared on lookup) so hash collisions // can't return the wrong run. - // - // The map owns its key string, but lookups happen every frame on the - // onBuild path. To avoid copying the string into a key on each cache - // hit, the hash and equality functors are transparent (is_transparent) - // and accept a borrowing view key whose text is a std::string_view; - // the owning std::string is only materialised on a miss, for emplace. struct ShapedRunKey { const Font* font; float pxSize; std::array color; std::string text; - }; - struct ShapedRunViewKey { - const Font* font; - float pxSize; - std::array color; - std::string_view text; + bool operator==(const ShapedRunKey&) const = default; }; struct ShapedRunKeyHash { - using is_transparent = void; - static std::size_t Hash(const Font* font, float pxSize, - const std::array& color, - std::string_view text) noexcept { - std::size_t h = std::hash{}(font); + std::size_t operator()(const ShapedRunKey& k) const noexcept { + std::size_t h = std::hash{}(k.font); auto mix = [&h](std::size_t v) noexcept { h ^= v + 0x9e3779b97f4a7c15ULL + (h << 6) + (h >> 2); }; - mix(std::hash{}(std::bit_cast(pxSize))); - for (float c : color) + mix(std::hash{}(std::bit_cast(k.pxSize))); + for (float c : k.color) mix(std::hash{}(std::bit_cast(c))); - mix(std::hash{}(text)); + mix(std::hash{}(k.text)); return h; } - std::size_t operator()(const ShapedRunKey& k) const noexcept { - return Hash(k.font, k.pxSize, k.color, k.text); - } - std::size_t operator()(const ShapedRunViewKey& k) const noexcept { - return Hash(k.font, k.pxSize, k.color, k.text); - } - }; - struct ShapedRunKeyEqual { - using is_transparent = void; - static bool Eq(const Font* fa, float pa, const std::array& ca, - std::string_view ta, - const Font* fb, float pb, const std::array& cb, - std::string_view tb) noexcept { - return fa == fb && pa == pb && ca == cb && ta == tb; - } - bool operator()(const ShapedRunKey& a, const ShapedRunKey& b) const noexcept { - return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text); - } - bool operator()(const ShapedRunViewKey& a, const ShapedRunKey& b) const noexcept { - return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text); - } - bool operator()(const ShapedRunKey& a, const ShapedRunViewKey& b) const noexcept { - return Eq(a.font, a.pxSize, a.color, a.text, b.font, b.pxSize, b.color, b.text); - } }; struct ShapedRun { std::vector glyphs; // origin-relative float advance = 0; - // Position of this run's key in shapedRunsLru_ (front = most - // recently used). Lets a hit splice the entry to the front and an - // overflow pop the least-recently-used entry, both O(1). - std::list::iterator lruIt{}; }; // Soft cap. A pathological caller drawing a fresh string every frame - // would grow this without bound; on overflow we evict the single - // least-recently-used run (issue #123) rather than clearing the whole - // cache. The hot set of stable labels — reshaped every frame, so always - // near the front — stays resident, while a churn of unique strings - // (FPS counters, timers) only recycles the cold tail. Correctness is - // unaffected either way; this just avoids the periodic full-UI reshape - // spike the old clear() caused. + // would grow this without bound; on overflow we drop everything and + // rebuild (correctness is unaffected, only the hit rate after a flush). static constexpr std::size_t kMaxShapedRuns = 8192; - std::unordered_map shapedRuns_; - // LRU recency order for shapedRuns_; front = most recently used. Holds - // pointers to the keys owned by the map — stable across rehash because - // unordered_map is node-based — so eviction never re-hashes the world. - std::list shapedRunsLru_; + std::unordered_map shapedRuns_; ImageSlot outImageSlot_; ImageSlot fontAtlasImageSlot_; diff --git a/interfaces/Crafter.Graphics-UIComponents.cppm b/interfaces/Crafter.Graphics-UIComponents.cppm index 7948c80..ec8c346 100644 --- a/interfaces/Crafter.Graphics-UIComponents.cppm +++ b/interfaces/Crafter.Graphics-UIComponents.cppm @@ -1,6 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ export module Crafter.Graphics:UIComponents; import std; import :UI; diff --git a/interfaces/Crafter.Graphics-VulkanBuffer.cppm b/interfaces/Crafter.Graphics-VulkanBuffer.cppm index a200efb..c1d0c25 100644 --- a/interfaces/Crafter.Graphics-VulkanBuffer.cppm +++ b/interfaces/Crafter.Graphics-VulkanBuffer.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; @@ -13,48 +29,12 @@ import std; import :Device; namespace Crafter { - // Round a host-write flush range outward to nonCoherentAtomSize boundaries — - // the alignment vkFlushMappedMemoryRanges demands for a sub-buffer range - // (whole-buffer VK_WHOLE_SIZE calls sidestep it). `atom` is - // VkPhysicalDeviceLimits::nonCoherentAtomSize (a power of two ≥ 1) and - // `mappingSize` is the byte size the memory was mapped with (the allocation - // size). The start rounds down and the end rounds up to atom multiples; the - // end is then clamped to `mappingSize`. Clamping is what keeps the range - // valid even when `mappingSize` itself is not atom-aligned: the spec permits - // a non-atom-multiple size only when offset+size equals the mapping size, and - // the clamp lands exactly on that exception. Pure math, so it is unit-tested - // without a device. - export struct MappedFlushRange { VkDeviceSize offset; VkDeviceSize size; }; - export constexpr MappedFlushRange AlignMappedFlushRange( - VkDeviceSize offset, VkDeviceSize bytes, - VkDeviceSize atom, VkDeviceSize mappingSize) { - VkDeviceSize begin = (offset / atom) * atom; - VkDeviceSize end = ((offset + bytes + atom - 1) / atom) * atom; - if (end > mappingSize) end = mappingSize; - return { begin, end - begin }; - } - export class VulkanBufferBase { public: VkDeviceAddress address; std::uint32_t size; - // Byte size the memory was mapped with — equal to the allocation's - // VkMemoryRequirements::size, which is ≥ `size` and what ranged flushes - // clamp their upper bound to. Only meaningful for mapped buffers. - VkDeviceSize mappedSize = 0; VkBuffer buffer = VK_NULL_HANDLE; VkDeviceMemory memory; - // Property flags of the memory type actually chosen by GetMemoryType — - // not the requested flags. GetMemoryType may land a request without the - // COHERENT bit on a coherent type (and vice versa), so the flush/ - // invalidate paths gate on this recorded value, never on the request. - VkMemoryPropertyFlags memoryPropertyFlagsChosen = 0; - // Byte capacity the buffer was created with, and the usage flags it was - // created with. Resize reuses the allocation in place when a new - // request still fits within `capacity` and these immutable-at-create - // properties match, avoiding a destroy+reallocate. - std::uint32_t capacity = 0; - VkBufferUsageFlags2 usageFlagsCreated = 0; }; export template @@ -82,8 +62,6 @@ namespace Crafter { // available on every device — see Device::GetMemoryType. void Create(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count, VkMemoryPropertyFlags preferredPropertyFlags = 0) { size = count * sizeof(T); - capacity = size; - usageFlagsCreated = usageFlags; // Carry usage in the maintenance5 flags2 chain so 64-bit bits // (e.g. VK_BUFFER_USAGE_2_MEMORY_DECOMPRESSION_BIT_EXT, bit 35) @@ -101,12 +79,10 @@ namespace Crafter { VkMemoryRequirements memReqs; vkGetBufferMemoryRequirements(Device::device, buffer, &memReqs); - std::uint32_t memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits, memoryPropertyFlags, preferredPropertyFlags); - memoryPropertyFlagsChosen = Device::memoryProperties.memoryTypes[memoryTypeIndex].propertyFlags; VkMemoryAllocateInfo memAlloc { .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = memReqs.size, - .memoryTypeIndex = memoryTypeIndex + .memoryTypeIndex = Device::GetMemoryType(memReqs.memoryTypeBits, memoryPropertyFlags, preferredPropertyFlags) }; VkMemoryAllocateFlagsInfoKHR allocFlagsInfo { @@ -124,13 +100,6 @@ namespace Crafter { }; address = vkGetBufferDeviceAddress(Device::device, &addressInfo); - // Record the allocation's byte size (≥ `size`) for every buffer, - // not just mapped ones: UploadDeviceLocalRange's direct path flushes - // only the dirty sub-range and clamps its rounded-up upper bound to - // this (the nonCoherentAtomSize-aligned-end exception, see - // AlignMappedFlushRange). A non-mapped buffer never maps persistently, - // but its memory is still the allocation a transient map writes into. - mappedSize = memReqs.size; if constexpr(Mapped) { Device::CheckVkResult(vkMapMemory(Device::device, memory, 0, memReqs.size, 0, reinterpret_cast(&(VulkanBufferMappedConditional::value)))); } @@ -145,41 +114,9 @@ namespace Crafter { buffer = VK_NULL_HANDLE; } - // Like Clear(), but hands the destroy+free to Device's fence-keyed - // deletion queue instead of doing it immediately. Use when the buffer - // may still be read by an in-flight frame's GPU work — destroying it - // now would be a use-after-free, since frames are pipelined up to - // framesInFlight deep (issue #101). The handle is nulled immediately so - // this VulkanBuffer no longer owns it; vkFreeMemory (deferred) implicitly - // unmaps mapped memory, so no explicit vkUnmapMemory is needed here. - void DeferredClear() { - Device::EnqueueDeletion(buffer, memory); - buffer = VK_NULL_HANDLE; - } - void Resize(VkBufferUsageFlags2 usageFlags, VkMemoryPropertyFlags memoryPropertyFlags, std::uint32_t count, VkMemoryPropertyFlags preferredPropertyFlags = 0) { - // Reuse the existing allocation in place when the request still fits - // and the fixed-at-create properties match: usage flags are - // immutable after creation, and the memory type already chosen must - // still satisfy the required property flags. preferredPropertyFlags - // is a best-effort perf hint and does not affect correctness, so it - // is intentionally not part of the guard. The buffer handle (and its - // device address / mapped pointer) is preserved, only `size` shrinks - // to the new logical extent. - std::uint32_t requestedSize = count * sizeof(T); - if(buffer != VK_NULL_HANDLE - && requestedSize <= capacity - && usageFlags == usageFlagsCreated - && (memoryPropertyFlagsChosen & memoryPropertyFlags) == memoryPropertyFlags) { - size = requestedSize; - return; - } - // Defer the old allocation's destruction: an in-flight frame may - // still reference it (the #63 hazard, no longer masked by a - // per-frame wait-idle since #40). DeferredClear nulls the handle, - // so the Create below starts from a clean slate. if(buffer != VK_NULL_HANDLE) { - DeferredClear(); + Clear(); } Create(usageFlags, memoryPropertyFlags, count, preferredPropertyFlags); } @@ -225,195 +162,7 @@ namespace Crafter { ); } - // Upload `count` host elements from `src` into this buffer as - // device-local, GPU-read geometry, choosing placement at runtime via - // the #89 upload strategy (Device::PreferDirectDeviceWrite) and then - // recording a barrier from the upload to (dstStageMask, dstAccessMask) - // on `cmd`: - // ReBAR / UMA (direct) — allocate HOST_VISIBLE with DEVICE_LOCAL as a - // best-effort preference (GetMemoryType lands the host-visible - // device-local type that the strategy already proved exists), map - // transiently, memcpy, flush if the chosen type isn't coherent, - // unmap. No staging buffer: it would be pure overhead on a bar where - // the device-local heap is itself host-writable. - // No / small BAR (staged) — allocate pure DEVICE_LOCAL (+ TRANSFER_DST), - // fill a transient HOST_VISIBLE staging buffer, vkCmdCopyBuffer into - // this buffer, then hand the staging buffer to the fence-keyed - // deferred-deletion queue (#101/#102) so it outlives the copy submit. - // Requires !Mapped: the destination must be free to be device-local-only - // (a persistent map would force HOST_VISIBLE), so the direct path maps - // just long enough to write. A same-size re-upload reuses the allocation - // (Resize), so the device address stays stable across an in-place AS - // UPDATE refit that re-calls this. - void UploadDeviceLocal(VkBufferUsageFlags2 usageFlags, const T* src, std::uint32_t count, VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(!Mapped) { - VkDeviceSize bytes = static_cast(count) * sizeof(T); - VkAccessFlags srcAccessMask; - VkPipelineStageFlags srcStageMask; - if (Device::PreferDirectDeviceWrite(bytes)) { - Resize(usageFlags, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, count, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - void* mapped = nullptr; - Device::CheckVkResult(vkMapMemory(Device::device, memory, 0, VK_WHOLE_SIZE, 0, &mapped)); - std::memcpy(mapped, src, bytes); - // Match FlushDevice()'s gate: a non-coherent type needs an - // explicit flush before the device reads the written range. - if (!(memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { - VkMappedMemoryRange range { - .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, - .memory = memory, - .offset = 0, - .size = VK_WHOLE_SIZE - }; - vkFlushMappedMemoryRanges(Device::device, 1, &range); - } - vkUnmapMemory(Device::device, memory); - srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; - srcStageMask = VK_PIPELINE_STAGE_HOST_BIT; - } else { - Resize(usageFlags | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, count); - // Persistent staging instead of a Create+DeferredClear per call - // (issue #120): on no-/small-BAR hardware Mesh::Refit / - // RecordProceduralBuild hit this branch every frame for deforming - // meshes, so a fresh alloc+map+free cycle per upload was pure - // churn on exactly the hardware this path targets. Keep a small - // ring of staging buffers, each grown to a high-water mark and - // reused — reallocated (by Resize, which defers the outgrown - // allocation) only when a larger upload arrives. - // - // The ring is grown lazily, on first entry into this staged - // branch, so ReBAR/UMA hardware (which always takes the direct - // `if` branch above) never constructs a staging allocation it - // does not use. - // - // Why a ring and not one shared buffer: the vkCmdCopyBuffer below - // still reads the staging buffer after this call returns — the - // reason the old code used DeferredClear. With frames pipelined - // framesInFlight deep, overwriting one shared staging buffer next - // frame would clobber data the previous frame's copy is still - // reading. Indexing by frameCounter % framesInFlight gives each - // in-flight frame its own slot; a slot is rewritten only after - // framesInFlight frames have elapsed — the exact window after - // which single-queue submission order (the same guarantee the - // #101 deletion queue relies on) ensures that copy has completed. - const std::uint32_t ringSize = - Device::framesInFlight ? Device::framesInFlight : 1; - if (stagingRing.size() < ringSize) { - stagingRing.resize(ringSize); - } - VulkanBuffer& staging = - stagingRing[Device::frameCounter % ringSize]; - // SHADER_DEVICE_ADDRESS: Create always queries the buffer device - // address (and allocates with the device-address bit); the - // staging buffer's own address is otherwise unused. Resize reuses - // the existing allocation (and its mapped pointer) when the - // upload still fits the slot's high-water capacity. - staging.Resize(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, count); - std::memcpy(staging.value, src, bytes); - staging.FlushDevice(); - VkBufferCopy region { .srcOffset = 0, .dstOffset = 0, .size = bytes }; - vkCmdCopyBuffer(cmd, staging.buffer, buffer, 1, ®ion); - srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; - } - - VkBufferMemoryBarrier barrier = { - .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, - .srcAccessMask = srcAccessMask, - .dstAccessMask = dstAccessMask, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .buffer = buffer, - .offset = 0, - .size = VK_WHOLE_SIZE - }; - vkCmdPipelineBarrier(cmd, srcStageMask, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL); - } - - // Re-upload only the half-open sub-range [offset, offset+count) of an - // already-allocated device-local buffer — the dirty-range counterpart of - // UploadDeviceLocal. `src` points at the first changed element (not the - // start of the whole array); `offset` is that element's index in this - // buffer. The buffer must already exist at its full size (a prior - // UploadDeviceLocal / Build sized it): this never Resizes, so the device - // address stays stable and the untouched elements keep their last - // contents. Only the dirty bytes are written + flushed (direct path) or - // staged + copied (staged path), and the post-upload barrier covers only - // that sub-range — so a deforming-mesh refit that nudges a few vertices - // pays for those vertices, not a full-array re-upload + flush + copy (#119). - // - // The direct-vs-staged choice is read from the memory type the buffer was - // actually allocated with (HOST_VISIBLE → map + write in place; - // device-local-only → stage + GPU copy), NOT from the sub-range size: the - // allocation is fixed, so a small dirty range must not be mis-routed to a - // map of a non-host-visible buffer the way UploadDeviceLocal's size-based - // PreferDirectDeviceWrite check would. The staged path's GPU copy needs - // VK_BUFFER_USAGE_TRANSFER_DST_BIT on the destination — already present, - // since UploadDeviceLocal only allocates device-local-only memory on the - // staged path, where it adds that bit. - void UploadDeviceLocalRange(const T* src, std::uint32_t offset, std::uint32_t count, VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(!Mapped) { - VkDeviceSize byteOffset = static_cast(offset) * sizeof(T); - VkDeviceSize bytes = static_cast(count) * sizeof(T); - VkAccessFlags srcAccessMask; - VkPipelineStageFlags srcStageMask; - if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) { - void* mapped = nullptr; - Device::CheckVkResult(vkMapMemory(Device::device, memory, 0, VK_WHOLE_SIZE, 0, &mapped)); - std::memcpy(static_cast(mapped) + byteOffset, src, bytes); - // Non-coherent memory needs an explicit flush — but only of the - // sub-range actually written, rounded outward to - // nonCoherentAtomSize (and clamped to the allocation size). - if (!(memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { - MappedFlushRange r = AlignMappedFlushRange( - byteOffset, bytes, Device::nonCoherentAtomSize, mappedSize); - VkMappedMemoryRange range { - .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, - .memory = memory, - .offset = r.offset, - .size = r.size - }; - vkFlushMappedMemoryRanges(Device::device, 1, &range); - } - vkUnmapMemory(Device::device, memory); - srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; - srcStageMask = VK_PIPELINE_STAGE_HOST_BIT; - } else { - // Device-local-only: stage just the dirty elements and copy them - // into place at byteOffset. The staging buffer outlives the queued - // copy via the fence-keyed deletion queue (#101/#102), exactly as - // the full-buffer staged path does. - VulkanBuffer staging; - staging.Create(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, count); - std::memcpy(staging.value, src, bytes); - staging.FlushDevice(); - VkBufferCopy region { .srcOffset = 0, .dstOffset = byteOffset, .size = bytes }; - vkCmdCopyBuffer(cmd, staging.buffer, buffer, 1, ®ion); - staging.DeferredClear(); - srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - srcStageMask = VK_PIPELINE_STAGE_TRANSFER_BIT; - } - - // Order only the written sub-range before the consumer reads it. The - // untouched bytes were made visible by their own prior upload's - // barrier and are not written in this submit, so they need no fresh - // dependency even though the build reads the whole buffer. - VkBufferMemoryBarrier barrier = { - .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, - .srcAccessMask = srcAccessMask, - .dstAccessMask = dstAccessMask, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .buffer = buffer, - .offset = byteOffset, - .size = bytes - }; - vkCmdPipelineBarrier(cmd, srcStageMask, dstStageMask, 0, 0, NULL, 1, &barrier, 0, NULL); - } - void FlushDevice() requires(Mapped) { - // Coherent memory needs no explicit flush — host writes are - // automatically visible to the device. - if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) { - return; - } VkMappedMemoryRange range = { .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, .memory = memory, @@ -423,27 +172,6 @@ namespace Crafter { vkFlushMappedMemoryRanges(Device::device, 1, &range); } - // Flush only the host writes in [offset, offset+bytes) to the device, - // instead of the whole buffer. Use after touching a small sub-range - // (e.g. one descriptor in a multi-KB descriptor heap) so cache - // maintenance scales with the bytes actually written. The range is - // rounded outward to nonCoherentAtomSize as the Vulkan spec requires. - // No-op on coherent memory, same gate as the whole-buffer FlushDevice(). - void FlushDevice(VkDeviceSize offset, VkDeviceSize bytes) requires(Mapped) { - if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) { - return; - } - MappedFlushRange r = AlignMappedFlushRange( - offset, bytes, Device::nonCoherentAtomSize, mappedSize); - VkMappedMemoryRange range = { - .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, - .memory = memory, - .offset = r.offset, - .size = r.size - }; - vkFlushMappedMemoryRanges(Device::device, 1, &range); - } - void FlushDevice(VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask) requires(Mapped) { FlushDevice(); VkBufferMemoryBarrier barrier = { @@ -468,45 +196,7 @@ namespace Crafter { ); } - // Ranged variant of FlushDevice(cmd, ...): flushes only the host writes - // in [offset, offset+bytes) (rounded outward to nonCoherentAtomSize, and - // a no-op on coherent memory — same gate as the other FlushDevice - // overloads) and records the HOST->(dstStageMask, dstAccessMask) barrier. - // Use after writing a sub-range so the cache-flush cost scales with the - // bytes touched rather than the whole high-water capacity. The barrier - // itself still spans the whole buffer (VK_WHOLE_SIZE): the execution/ - // visibility dependency is cheap regardless of range, and only the - // flush's cache maintenance is bandwidth-sensitive. - void FlushDevice(VkCommandBuffer cmd, VkAccessFlags dstAccessMask, VkPipelineStageFlags dstStageMask, VkDeviceSize offset, VkDeviceSize bytes) requires(Mapped) { - FlushDevice(offset, bytes); - VkBufferMemoryBarrier barrier = { - .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_HOST_WRITE_BIT, - .dstAccessMask = dstAccessMask, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .buffer = buffer, - .offset = 0, - .size = VK_WHOLE_SIZE - }; - - vkCmdPipelineBarrier( - cmd, - VK_PIPELINE_STAGE_HOST_BIT, - dstStageMask, - 0, - 0, NULL, - 1, &barrier, - 0, NULL - ); - } - void FlushHost() requires(Mapped) { - // Coherent memory needs no explicit invalidate — device writes are - // automatically visible to the host. - if (memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) { - return; - } VkMappedMemoryRange range = { .sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE, .memory = memory, @@ -516,28 +206,12 @@ namespace Crafter { vkInvalidateMappedMemoryRanges(Device::device, 1, &range); } - // Persistent per-frame-in-flight staging ring for the staged - // UploadDeviceLocal path (issue #120). Empty — and so holding zero - // host-visible allocations — until the first upload that actually - // stages, which only happens on no-/small-BAR hardware (ReBAR/UMA always - // takes the direct-write branch and never touches this, so the lazy ring - // keeps the change a no-op there). Sized to Device::framesInFlight so - // each in-flight frame copies from its own slot; each slot grows to a - // high-water mark via Resize and is reused across frames rather than - // reallocated every call — mirroring the TLAS instance/metadata reuse. - std::vector> stagingRing; - VulkanBuffer(VulkanBuffer&& other) { buffer = other.buffer; memory = other.memory; size = other.size; - mappedSize = other.mappedSize; - capacity = other.capacity; - usageFlagsCreated = other.usageFlagsCreated; - memoryPropertyFlagsChosen = other.memoryPropertyFlagsChosen; other.buffer = VK_NULL_HANDLE; address = other.address; - stagingRing = std::move(other.stagingRing); if constexpr(Mapped) { VulkanBufferMappedConditional::value = other.VulkanBufferMappedConditional::value; } diff --git a/interfaces/Crafter.Graphics-VulkanTransition.cppm b/interfaces/Crafter.Graphics-VulkanTransition.cppm index 23d3bc5..070bdf6 100644 --- a/interfaces/Crafter.Graphics-VulkanTransition.cppm +++ b/interfaces/Crafter.Graphics-VulkanTransition.cppm @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM diff --git a/interfaces/Crafter.Graphics-WebGPU.cppm b/interfaces/Crafter.Graphics-WebGPU.cppm index 513efb2..0cc81c9 100644 --- a/interfaces/Crafter.Graphics-WebGPU.cppm +++ b/interfaces/Crafter.Graphics-WebGPU.cppm @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® @@ -34,23 +31,6 @@ namespace Crafter::WebGPU { __attribute__((import_module("env"), import_name("wgpuInit"))) extern "C" void wgpuInit(); - // Reparent the render canvas into a host DOM element so a scene renders - // inline inside the app's own layout rather than as a full-page layer. - // `idPtr`/`idLen` is a UTF-8 string naming an element id; an empty id - // detaches the canvas back to and hides it. Implemented in - // additional/dom-webgpu.js. Prefer the SetCanvasMount() wrapper below. - __attribute__((import_module("env"), import_name("wgpuSetCanvasMount"))) - extern "C" void wgpuSetCanvasMount(const void* idPtr, std::int32_t idLen); - - // Mount the render canvas into the DOM element with id `elementId` - // (the render surface is sized to that element each frame). Pass an - // empty string to detach and hide the canvas, leaving a plain DOM page. - // Safe to call any time after the module has started — e.g. from a - // route handler once the target element exists in the DOM. - export inline void SetCanvasMount(std::string_view elementId) { - wgpuSetCanvasMount(elementId.data(), static_cast(elementId.size())); - } - __attribute__((import_module("env"), import_name("wgpuCreateBuffer"))) extern "C" std::uint32_t wgpuCreateBuffer(std::int32_t byteSize); __attribute__((import_module("env"), import_name("wgpuWriteBuffer"))) diff --git a/interfaces/Crafter.Graphics-WebGPUBuffer.cppm b/interfaces/Crafter.Graphics-WebGPUBuffer.cppm index 9ac5832..4dec146 100644 --- a/interfaces/Crafter.Graphics-WebGPUBuffer.cppm +++ b/interfaces/Crafter.Graphics-WebGPUBuffer.cppm @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® @@ -105,18 +102,10 @@ export namespace Crafter { FlushDeviceRange(off, off, kStride); } - // Schedule a GPU→CPU readback of this buffer. Asynchronous; data - // isn't ready until a later PollReadback returns true. Successive - // Enqueues without a Poll are dropped — they're a no-op while the - // previous map is in flight. - // - // `byteCount` (0 = the whole buffer) bounds the readback to the - // live prefix: an over-provisioned event-queue buffer need only - // copy its header / used span GPU→staging→wasm, not the full - // capacity, every drain. Clamped to `size`. The staging buffer is - // sized to the full capacity, so the prefix length may vary from - // one Enqueue to the next; pass the SAME byteCount to the matching - // PollReadback so the right number of bytes lands in `.value`. + // Schedule a GPU→CPU readback of this buffer's entire contents. + // Asynchronous; data isn't ready until a later PollReadback + // returns true. Successive Enqueues without a Poll are dropped + // — they're a no-op while the previous map is in flight. // // `resetBytes` ≥ 0 — if non-zero, the first `resetBytes` bytes // of THIS buffer are clearBuffer-cleared on the GPU command @@ -125,22 +114,17 @@ export namespace Crafter { // The reset is tied to a successful enqueue (skipped enqueue = // skipped reset), preserving accumulated state across missed // drains. - void EnqueueReadback(std::uint32_t resetBytes = 0, std::uint32_t byteCount = 0) { - const std::uint32_t n = (byteCount == 0 || byteCount > size) ? size : byteCount; + void EnqueueReadback(std::uint32_t resetBytes = 0) { WebGPU::wgpuReadbackEnqueue(handle, - static_cast(n), + static_cast(size), static_cast(resetBytes)); } // Try to copy the readback bytes into this->value. Returns true // if the previous EnqueueReadback resolved and the data is now // mirrored into .value; false if the map is still pending. - // `byteCount` must match the value passed to the paired - // EnqueueReadback (0 = whole buffer); it bounds the staging→wasm - // copy to the live prefix that was captured. - bool PollReadback(std::uint32_t byteCount = 0) requires(Mapped) { - const std::uint32_t n = (byteCount == 0 || byteCount > size) ? size : byteCount; + bool PollReadback() requires(Mapped) { return WebGPU::wgpuReadbackPoll(handle, this->value, - static_cast(n)) != 0; + static_cast(size)) != 0; } // Non-consuming readiness probe. Returns true if a subsequent // PollReadback would succeed without changing state otherwise. diff --git a/interfaces/Crafter.Graphics-WebGPUComputeShader.cppm b/interfaces/Crafter.Graphics-WebGPUComputeShader.cppm index 96fc521..58f0f77 100644 --- a/interfaces/Crafter.Graphics-WebGPUComputeShader.cppm +++ b/interfaces/Crafter.Graphics-WebGPUComputeShader.cppm @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - /* Crafter®.Graphics Copyright (C) 2026 Catcrafts® diff --git a/interfaces/Crafter.Graphics-Window.cppm b/interfaces/Crafter.Graphics-Window.cppm index 37e97f4..0676b96 100644 --- a/interfaces/Crafter.Graphics-Window.cppm +++ b/interfaces/Crafter.Graphics-Window.cppm @@ -1,6 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ module; #ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND @@ -121,22 +136,7 @@ export namespace Crafter { ~Window(); #endif - // Enter the platform event loop and never return. In DOM mode this - // hands the loop to requestAnimationFrame and _Exit(0)s so the wasm - // instance survives without running static destructors. No caller - // code after this point ever runs, on any target. void StartSync(); - - // As StartSync, but does NOT start the animation-frame loop in DOM - // mode — for event-driven pages that only touch the DOM and never - // draw. A rAF tick running forever to do nothing costs battery in - // every open tab. Still never returns. - // - // On native this is identical to StartSync: the event loop is what - // keeps a window alive there, so there is no cheaper option. Call it - // unconditionally and get the browser saving without an #ifdef. - void StayAlive(); - void StartUpdate(); void StopUpdate(); void SetTitle(const std::string_view title); diff --git a/interfaces/Crafter.Graphics.cppm b/interfaces/Crafter.Graphics.cppm index 6b359f7..5d136d9 100644 --- a/interfaces/Crafter.Graphics.cppm +++ b/interfaces/Crafter.Graphics.cppm @@ -1,5 +1,22 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +Catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3.0 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ export module Crafter.Graphics; diff --git a/project.cpp b/project.cpp index 7342e2f..bbd1ec3 100644 --- a/project.cpp +++ b/project.cpp @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - import std; import Crafter.Build; namespace fs = std::filesystem; @@ -9,15 +6,6 @@ using namespace Crafter; extern "C" Configuration CrafterBuildProject(std::span args) { std::vector depArgs(args.begin(), args.end()); - // Crafter.Event/Math/Asset are always resolved from forgejo (GitProject); - // there is no local-sibling branch for them here. A consumer building - // THIS project with --local (to pick up local Crafter.Graphics edits) - // forwards --local down through depArgs, but these git deps can't honour - // it — the flag makes their resolver look for an unsuffixed local - // checkout that doesn't exist. Strip it so the sub-deps resolve normally - // while Crafter.Graphics itself is still built from the local tree. - std::erase(depArgs, "--local"); - Configuration* event = GitProject({ .source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Event.git" }, .args = depArgs, @@ -192,31 +180,7 @@ extern "C" Configuration CrafterBuildProject(std::span a // JS glue shipped alongside the .wasm so the loader has the // env-import surface the Window/Dom bindings expect. cfg.files.emplace_back(fs::path("additional/dom-env.js")); - - // dom-webgpu.js is opt-out via --no-webgpu, for apps that only use - // the Dom/Router partitions and never touch Device/RTPass/UIRenderer. - // - // Three reasons a DOM-only app wants it gone, in increasing order of - // severity: - // 1. It is ~182 KB (47 KB gzip) of dead weight. - // 2. Its init is an async IIFE that runtime.js awaits before - // _start(), and that init does `await navigator.gpu - // .requestAdapter()` — so every page load blocks wasm startup on - // a GPU adapter request, including on machines with no GPU. - // 3. Worst: when navigator.gpu is absent it REPLACES - // document.body.innerHTML with an error message and throws, so - // _start() never runs. For a server-rendered page that means the - // real content is destroyed and replaced with a WebGPU warning on - // any browser without WebGPU support. - // - // Safety: with --gc-sections an app that never calls into the WebGPU - // partitions emits no wgpu* env imports, so omitting the bridge links - // and instantiates cleanly. An app that DOES use WebGPU and passes - // --no-webgpu anyway fails at instantiate with a missing-import error - // naming the specific wgpu* symbol. - if (!opts.Has("--no-webgpu")) { - cfg.files.emplace_back(fs::path("additional/dom-webgpu.js")); - } + cfg.files.emplace_back(fs::path("additional/dom-webgpu.js")); } else { std::array impls = { "implementations/Crafter.Graphics-Clipboard", @@ -240,7 +204,6 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.shaders.emplace_back(fs::path("shaders/ui-circles.comp.glsl"), std::string("main"), ShaderType::Compute); cfg.shaders.emplace_back(fs::path("shaders/ui-images.comp.glsl"), std::string("main"), ShaderType::Compute); cfg.shaders.emplace_back(fs::path("shaders/ui-text.comp.glsl"), std::string("main"), ShaderType::Compute); - cfg.shaders.emplace_back(fs::path("shaders/ui-fused.comp.glsl"), std::string("main"), ShaderType::Compute); cfg.buildFiles.emplace_back(fs::path("shaders/ui-shared.glsl")); // Regression test for issue #18: drive the NVIDIA descriptor-heap @@ -332,41 +295,6 @@ extern "C" Configuration CrafterBuildProject(std::span a frameLoopImpls.emplace_back("tests/FrameLoopSync/main"); fl.GetInterfacesAndImplementations(ifaces, frameLoopImpls); cfg.tests.push_back(std::move(frameLoopTest)); - - // Issue #153: two frame-loop validation errors. (1) The acquire - // barrier hardcoded dstAccessMask = SHADER_WRITE|TRANSFER_WRITE but - // used the per-pass stage union (COMPUTE_SHADER for an all-compute - // frame) as its dst stage, so TRANSFER_WRITE was unsupported and - // VUID-02820 fired every frame — FrameLoopSync can't see it because - // it runs with no passes (the union falls back to the conservative - // writer set, which includes TRANSFER). (2) Mid-session - // StartInit/FinishInit reuse the shared draw command buffer while - // the loop's last submission of it is still in flight, re-beginning - // (00049) and re-submitting (00071) a pending buffer. Drives a real - // compute pass through the loop plus interleaved StartInit rounds - // and asserts the layer stays silent. Needs the Wayland backend + a - // real compositor, so it lives in the !windows block. - Test setupReuseTest; - Configuration& sr = setupReuseTest.config; - sr.path = cfg.path; - sr.name = "SetupCmdBufferReuse"; - sr.outputName = "SetupCmdBufferReuse"; - sr.type = ConfigurationType::Executable; - sr.target = cfg.target; - sr.march = cfg.march; - sr.mtune = cfg.mtune; - sr.debug = cfg.debug; - sr.sysroot = cfg.sysroot; - sr.dependencies = cfg.dependencies; - sr.externalDependencies = cfg.externalDependencies; - sr.compileFlags = cfg.compileFlags; - sr.linkFlags = cfg.linkFlags; - sr.defines = cfg.defines; - sr.cFiles = cfg.cFiles; - std::vector setupReuseImpls(impls.begin(), impls.end()); - setupReuseImpls.emplace_back("tests/SetupCmdBufferReuse/main"); - sr.GetInterfacesAndImplementations(ifaces, setupReuseImpls); - cfg.tests.push_back(std::move(setupReuseTest)); } // Issue #36: BLAS build options. Drives the real hardware AS-build @@ -399,70 +327,6 @@ extern "C" Configuration CrafterBuildProject(std::span a bc.GetInterfacesAndImplementations(ifaces, blasImpls); cfg.tests.push_back(std::move(blasTest)); - // Issue #64: TLAS host-input buffers (instanceBuffer / metadataBuffer) - // grow on a high-water mark instead of reallocating to the exact count - // every topology change. Drives the real hardware AS-build path — a - // cube BLAS plus RenderingElement3D::BuildTLAS at a sequence of - // instance counts — and asserts that a shrink (and any growth within - // the high-water capacity) reuses the existing allocation while a - // growth past it reallocates, with the validation layer reporting no - // errors when an oversized instance buffer is fed to the build. Needs a - // Vulkan RT device at runtime, so it shares the native build settings. - Test tlasTest; - Configuration& tlc = tlasTest.config; - tlc.path = cfg.path; - tlc.name = "TLASHighWaterMark"; - tlc.outputName = "TLASHighWaterMark"; - tlc.type = ConfigurationType::Executable; - tlc.target = cfg.target; - tlc.march = cfg.march; - tlc.mtune = cfg.mtune; - tlc.debug = cfg.debug; - tlc.sysroot = cfg.sysroot; - tlc.dependencies = cfg.dependencies; - tlc.externalDependencies = cfg.externalDependencies; - tlc.compileFlags = cfg.compileFlags; - tlc.linkFlags = cfg.linkFlags; - tlc.defines = cfg.defines; - tlc.cFiles = cfg.cFiles; - std::vector tlasImpls(impls.begin(), impls.end()); - tlasImpls.emplace_back("tests/TLASHighWaterMark/main"); - tlc.GetInterfacesAndImplementations(ifaces, tlasImpls); - cfg.tests.push_back(std::move(tlasTest)); - - // Issue #118: the per-frame TLAS instance+metadata host rebuild copies - // (and flushes) only the slots whose element's host-authored data - // changed, tracked via RenderingElement3D::hostDataVersion against the - // per-frame uploadedVersion record; untracked elements (version 0) keep - // the always-copy behaviour. Drives the real AS-build path — a cube - // BLAS plus BuildTLAS at a sequence of marks/mutations — and reads back - // the host-mapped buffers to assert that clean slots are skipped, dirty - // slots re-uploaded, and relocation on the refit path re-uploads exactly - // the moved slots, with the validation layer reporting no errors over - // the ranged FlushDevice the dirty span feeds. Needs a Vulkan RT device - // at runtime, so it shares the native build settings. - Test tlasDirtyTest; - Configuration& tld = tlasDirtyTest.config; - tld.path = cfg.path; - tld.name = "TLASInstanceDirtyTracking"; - tld.outputName = "TLASInstanceDirtyTracking"; - tld.type = ConfigurationType::Executable; - tld.target = cfg.target; - tld.march = cfg.march; - tld.mtune = cfg.mtune; - tld.debug = cfg.debug; - tld.sysroot = cfg.sysroot; - tld.dependencies = cfg.dependencies; - tld.externalDependencies = cfg.externalDependencies; - tld.compileFlags = cfg.compileFlags; - tld.linkFlags = cfg.linkFlags; - tld.defines = cfg.defines; - tld.cFiles = cfg.cFiles; - std::vector tlasDirtyImpls(impls.begin(), impls.end()); - tlasDirtyImpls.emplace_back("tests/TLASInstanceDirtyTracking/main"); - tld.GetInterfacesAndImplementations(ifaces, tlasDirtyImpls); - cfg.tests.push_back(std::move(tlasDirtyTest)); - // Issue #51: FontAtlas only re-uploads the dirty sub-rect now, // tracked via FontAtlas::DirtyRect. The accumulation/clamp math is // pure CPU, so this test drives it directly — no GPU device needed @@ -550,360 +414,6 @@ extern "C" Configuration CrafterBuildProject(std::span a mc.GetInterfacesAndImplementations(ifaces, memImpls); cfg.tests.push_back(std::move(memTest)); - // Issue #70: ImageVulkan's mip-chain upload folds the never-read final - // blit destination into a single batched final-transition barrier - // instead of giving it a dedicated DST->SRC barrier. The barrier set is - // built by BuildMipChainFinalBarriers, pure CPU logic over mip count and - // layout, so this test drives it directly — no GPU device at runtime. - Test mipBarrierTest; - Configuration& mbc = mipBarrierTest.config; - mbc.path = cfg.path; - mbc.name = "MipChainBarrierBatch"; - mbc.outputName = "MipChainBarrierBatch"; - mbc.type = ConfigurationType::Executable; - mbc.target = cfg.target; - mbc.march = cfg.march; - mbc.mtune = cfg.mtune; - mbc.debug = cfg.debug; - mbc.sysroot = cfg.sysroot; - mbc.dependencies = cfg.dependencies; - mbc.externalDependencies = cfg.externalDependencies; - mbc.compileFlags = cfg.compileFlags; - mbc.linkFlags = cfg.linkFlags; - mbc.defines = cfg.defines; - mbc.cFiles = cfg.cFiles; - std::vector mipBarrierImpls(impls.begin(), impls.end()); - mipBarrierImpls.emplace_back("tests/MipChainBarrierBatch/main"); - mbc.GetInterfacesAndImplementations(ifaces, mipBarrierImpls); - cfg.tests.push_back(std::move(mipBarrierTest)); - - // Issue #115: the frame loop's inter-pass and acquire/present barriers - // no longer use ALL_COMMANDS / a queue-wide VkMemoryBarrier. The stage - // masks are derived per pass via RenderPass::SwapchainStage() (compute - // vs ray-tracing) and unioned across the frame by SwapchainStageUnion, - // and the inter-pass dependency is scoped to the swapchain image by - // BuildSwapchainInterPassBarrier — all pure CPU logic over the pass - // list, so this test drives them directly with no GPU at runtime. - Test swapBarrierTest; - Configuration& sbc = swapBarrierTest.config; - sbc.path = cfg.path; - sbc.name = "SwapchainBarrierScope"; - sbc.outputName = "SwapchainBarrierScope"; - sbc.type = ConfigurationType::Executable; - sbc.target = cfg.target; - sbc.march = cfg.march; - sbc.mtune = cfg.mtune; - sbc.debug = cfg.debug; - sbc.sysroot = cfg.sysroot; - sbc.dependencies = cfg.dependencies; - sbc.externalDependencies = cfg.externalDependencies; - sbc.compileFlags = cfg.compileFlags; - sbc.linkFlags = cfg.linkFlags; - sbc.defines = cfg.defines; - sbc.cFiles = cfg.cFiles; - std::vector swapBarrierImpls(impls.begin(), impls.end()); - swapBarrierImpls.emplace_back("tests/SwapchainBarrierScope/main"); - sbc.GetInterfacesAndImplementations(ifaces, swapBarrierImpls); - cfg.tests.push_back(std::move(swapBarrierTest)); - - // Issue #47: the fused UI uber-kernel (shaders/ui-fused.comp.glsl) and - // its C++ push-constant mirror UIFusedHeader. Compiles the real shader - // with glslang, validates with spirv-val, and pins the push-constant - // member offsets to UIFusedHeader's layout so a GLSL/C++ drift can't - // slip through (the C++ static_assert only guards the C++ side). No GPU - // device at runtime, but glslang + spirv-val are required tools. - Test fusedTest; - Configuration& ftc = fusedTest.config; - ftc.path = cfg.path; - ftc.name = "UIFusedShader"; - ftc.outputName = "UIFusedShader"; - ftc.type = ConfigurationType::Executable; - ftc.target = cfg.target; - ftc.march = cfg.march; - ftc.mtune = cfg.mtune; - ftc.debug = cfg.debug; - ftc.sysroot = cfg.sysroot; - ftc.dependencies = cfg.dependencies; - ftc.externalDependencies = cfg.externalDependencies; - ftc.compileFlags = cfg.compileFlags; - ftc.linkFlags = cfg.linkFlags; - ftc.defines = cfg.defines; - ftc.cFiles = cfg.cFiles; - std::vector fusedImpls(impls.begin(), impls.end()); - fusedImpls.emplace_back("tests/UIFusedShader/main"); - ftc.GetInterfacesAndImplementations(ifaces, fusedImpls); - fusedTest.requires_ = { "tool:glslang", "tool:spirv-val" }; - cfg.tests.push_back(std::move(fusedTest)); - - // Issue #60: VulkanBuffer::FlushDevice / FlushHost now record the chosen - // memory type's propertyFlags at Create time and skip the - // flush/invalidate when the memory is HOST_COHERENT. The gate is pure - // logic over the recorded flags, so this test stamps them directly and - // verifies the coherent path issues no Vulkan call — no GPU device - // needed at runtime. - Test flushTest; - Configuration& fc = flushTest.config; - fc.path = cfg.path; - fc.name = "VulkanBufferFlushGate"; - fc.outputName = "VulkanBufferFlushGate"; - fc.type = ConfigurationType::Executable; - fc.target = cfg.target; - fc.march = cfg.march; - fc.mtune = cfg.mtune; - fc.debug = cfg.debug; - fc.sysroot = cfg.sysroot; - fc.dependencies = cfg.dependencies; - fc.externalDependencies = cfg.externalDependencies; - fc.compileFlags = cfg.compileFlags; - fc.linkFlags = cfg.linkFlags; - fc.defines = cfg.defines; - fc.cFiles = cfg.cFiles; - std::vector flushImpls(impls.begin(), impls.end()); - flushImpls.emplace_back("tests/VulkanBufferFlushGate/main"); - fc.GetInterfacesAndImplementations(ifaces, flushImpls); - cfg.tests.push_back(std::move(flushTest)); - - // Ranged FlushDevice: UI descriptor registration now flushes only the - // written descriptor byte range instead of the whole heap, rounding the - // range outward to nonCoherentAtomSize via AlignMappedFlushRange. The - // rounding is pure math and the coherent gate is pure logic, so this - // test drives both directly with no GPU device. - Test rangedFlushTest; - Configuration& rfc = rangedFlushTest.config; - rfc.path = cfg.path; - rfc.name = "VulkanBufferRangedFlush"; - rfc.outputName = "VulkanBufferRangedFlush"; - rfc.type = ConfigurationType::Executable; - rfc.target = cfg.target; - rfc.march = cfg.march; - rfc.mtune = cfg.mtune; - rfc.debug = cfg.debug; - rfc.sysroot = cfg.sysroot; - rfc.dependencies = cfg.dependencies; - rfc.externalDependencies = cfg.externalDependencies; - rfc.compileFlags = cfg.compileFlags; - rfc.linkFlags = cfg.linkFlags; - rfc.defines = cfg.defines; - rfc.cFiles = cfg.cFiles; - std::vector rangedFlushImpls(impls.begin(), impls.end()); - rangedFlushImpls.emplace_back("tests/VulkanBufferRangedFlush/main"); - rfc.GetInterfacesAndImplementations(ifaces, rangedFlushImpls); - cfg.tests.push_back(std::move(rangedFlushTest)); - - // Issue #63: VulkanBuffer::Resize now reuses the existing allocation in - // place when a new request still fits the created capacity and the - // immutable-at-create properties match (usage flags fixed at create; - // chosen memory type still satisfies the required flags), instead of - // always destroying + reallocating. The reuse guard is pure logic over - // recorded fields, so this test stamps a fake handle + capacity/flags and - // verifies the in-place path issues no Vulkan call — no GPU device needed. - Test resizeTest; - Configuration& rc = resizeTest.config; - rc.path = cfg.path; - rc.name = "VulkanBufferResizeReuse"; - rc.outputName = "VulkanBufferResizeReuse"; - rc.type = ConfigurationType::Executable; - rc.target = cfg.target; - rc.march = cfg.march; - rc.mtune = cfg.mtune; - rc.debug = cfg.debug; - rc.sysroot = cfg.sysroot; - rc.dependencies = cfg.dependencies; - rc.externalDependencies = cfg.externalDependencies; - rc.compileFlags = cfg.compileFlags; - rc.linkFlags = cfg.linkFlags; - rc.defines = cfg.defines; - rc.cFiles = cfg.cFiles; - std::vector resizeImpls(impls.begin(), impls.end()); - resizeImpls.emplace_back("tests/VulkanBufferResizeReuse/main"); - rc.GetInterfacesAndImplementations(ifaces, resizeImpls); - cfg.tests.push_back(std::move(resizeTest)); - - // Issue #101: fence-keyed deferred resource-deletion queue. Since #40 - // dropped the per-frame wait-idle, destroying a buffer the GPU may - // still read (Resize's reallocate path) is a use-after-free. - // VulkanBuffer::DeferredClear / Resize now hand handles to Device's - // queue, which ReclaimDeletions frees only after framesInFlight frames - // and DrainDeletions frees on a wait-idle. The retire timing is driven - // on a real headless device (real buffers so the frees execute and the - // validation layer can object) by stepping Device::frameCounter — no - // swapchain/window needed, so it shares the native build settings. - Test deferredTest; - Configuration& dc = deferredTest.config; - dc.path = cfg.path; - dc.name = "DeferredDeletion"; - dc.outputName = "DeferredDeletion"; - dc.type = ConfigurationType::Executable; - dc.target = cfg.target; - dc.march = cfg.march; - dc.mtune = cfg.mtune; - dc.debug = cfg.debug; - dc.sysroot = cfg.sysroot; - dc.dependencies = cfg.dependencies; - dc.externalDependencies = cfg.externalDependencies; - dc.compileFlags = cfg.compileFlags; - dc.linkFlags = cfg.linkFlags; - dc.defines = cfg.defines; - dc.cFiles = cfg.cFiles; - std::vector deferredImpls(impls.begin(), impls.end()); - deferredImpls.emplace_back("tests/DeferredDeletion/main"); - dc.GetInterfacesAndImplementations(ifaces, deferredImpls); - cfg.tests.push_back(std::move(deferredTest)); - - // Issue #67: the compressed Mesh::Build path no longer pins its - // host-visible `compressedStaging` for the mesh's life — it releases it - // via DeferredClear() right after recording the GPU decompress, so the - // fence-keyed deletion queue (#101/#102) frees it once that submit's - // frame has cleared. Drives the real VK_EXT_memory_decompression / - // GDeflate path on a headless device (no swapchain/window — a decompress - // + BLAS build only needs the queue + command pool) and asserts the - // staging is enqueued (not pinned), the build is validation-clean with - // correct decompressed data, and the entry retires only after - // framesInFlight frames. Shares the native build settings; needs the - // asset pipeline for SaveCompressed/LoadCompressedMesh (cfg.dependencies - // already carries Crafter.Asset). - Test meshStagingTest; - Configuration& msc = meshStagingTest.config; - msc.path = cfg.path; - msc.name = "MeshDecompressStagingRelease"; - msc.outputName = "MeshDecompressStagingRelease"; - msc.type = ConfigurationType::Executable; - msc.target = cfg.target; - msc.march = cfg.march; - msc.mtune = cfg.mtune; - msc.debug = cfg.debug; - msc.sysroot = cfg.sysroot; - msc.dependencies = cfg.dependencies; - msc.externalDependencies = cfg.externalDependencies; - msc.compileFlags = cfg.compileFlags; - msc.linkFlags = cfg.linkFlags; - msc.defines = cfg.defines; - msc.cFiles = cfg.cFiles; - std::vector meshStagingImpls(impls.begin(), impls.end()); - meshStagingImpls.emplace_back("tests/MeshDecompressStagingRelease/main"); - msc.GetInterfacesAndImplementations(ifaces, meshStagingImpls); - cfg.tests.push_back(std::move(meshStagingTest)); - - // Issue #114: a static ImageVulkan no longer pins its host-visible - // staging `buffer` for the image's life — Update releases it via - // DeferredClear() right after recording the buffer→image copy, so the - // fence-keyed deletion queue (#101/#102) frees it once that submit's - // frame clears, while a `streamed` image (the FontAtlas) keeps its - // persistent map. Drives the real upload path on a headless device (no - // swapchain/window — a buffer→image copy + readback only needs the queue - // + command pool) and asserts the staging is enqueued (not pinned), the - // image reads back byte-equal (the released staging outlived the submit), - // the entry retires only after framesInFlight frames, and a streamed - // image keeps + then frees its staging on Destroy. Shares the native - // build settings. - Test imageStagingTest; - Configuration& isc = imageStagingTest.config; - isc.path = cfg.path; - isc.name = "ImageStagingRelease"; - isc.outputName = "ImageStagingRelease"; - isc.type = ConfigurationType::Executable; - isc.target = cfg.target; - isc.march = cfg.march; - isc.mtune = cfg.mtune; - isc.debug = cfg.debug; - isc.sysroot = cfg.sysroot; - isc.dependencies = cfg.dependencies; - isc.externalDependencies = cfg.externalDependencies; - isc.compileFlags = cfg.compileFlags; - isc.linkFlags = cfg.linkFlags; - isc.defines = cfg.defines; - isc.cFiles = cfg.cFiles; - std::vector imageStagingImpls(impls.begin(), impls.end()); - imageStagingImpls.emplace_back("tests/ImageStagingRelease/main"); - isc.GetInterfacesAndImplementations(ifaces, imageStagingImpls); - cfg.tests.push_back(std::move(imageStagingTest)); - - // Issue #89: Device::PreferDirectDeviceWrite chooses the upload strategy - // for a CPU-written, GPU-read buffer — direct HOST_VISIBLE|DEVICE_LOCAL - // map+write on ReBAR/UMA vs. staged-into-pure-DEVICE_LOCAL on a small - // BAR window (#58). The decision is pure CPU logic over - // Device::memoryProperties (types + heaps), so this test installs - // synthetic ReBAR / UMA / small-window / no-BAR layouts and drives it - // directly — no GPU device needed at runtime. - Test uploadTest; - Configuration& uc = uploadTest.config; - uc.path = cfg.path; - uc.name = "UploadStrategy"; - uc.outputName = "UploadStrategy"; - uc.type = ConfigurationType::Executable; - uc.target = cfg.target; - uc.march = cfg.march; - uc.mtune = cfg.mtune; - uc.debug = cfg.debug; - uc.sysroot = cfg.sysroot; - uc.dependencies = cfg.dependencies; - uc.externalDependencies = cfg.externalDependencies; - uc.compileFlags = cfg.compileFlags; - uc.linkFlags = cfg.linkFlags; - uc.defines = cfg.defines; - uc.cFiles = cfg.cFiles; - std::vector uploadImpls(impls.begin(), impls.end()); - uploadImpls.emplace_back("tests/UploadStrategy/main"); - uc.GetInterfacesAndImplementations(ifaces, uploadImpls); - cfg.tests.push_back(std::move(uploadTest)); - - // Issue #57: Font::GetLineWidth memoises per-codepoint advances in - // font units (Font::AdvanceUnits) and rescales per call, instead of - // calling stbtt_GetCodepointHMetrics for every glyph on every caret - // query. Pure CPU — Font only touches stb_truetype — so this drives - // the public API directly with no Vulkan device. The font file is - // copied next to the binary; the test also probes the project root. - Test advTest; - Configuration& vc = advTest.config; - vc.path = cfg.path; - vc.name = "FontAdvanceCache"; - vc.outputName = "FontAdvanceCache"; - vc.type = ConfigurationType::Executable; - vc.target = cfg.target; - vc.march = cfg.march; - vc.mtune = cfg.mtune; - vc.debug = cfg.debug; - vc.sysroot = cfg.sysroot; - vc.dependencies = cfg.dependencies; - vc.externalDependencies = cfg.externalDependencies; - vc.compileFlags = cfg.compileFlags; - vc.linkFlags = cfg.linkFlags; - vc.defines = cfg.defines; - vc.cFiles = cfg.cFiles; - vc.files = { fs::path("tests/FontAdvanceCache/font.ttf") }; - std::vector advImpls(impls.begin(), impls.end()); - advImpls.emplace_back("tests/FontAdvanceCache/main"); - vc.GetInterfacesAndImplementations(ifaces, advImpls); - cfg.tests.push_back(std::move(advTest)); - - // Issue #50: uiResolveScreenPixel now gates its per-pixel clip-rect - // compares on the reserved kUIFlagClip bit, which FillHeader sets only - // when the clip rect is narrower than the surface. The decision lives - // in UIRenderer::ClipFlags — pure CPU logic over the clip rect and the - // surface size — so this test drives it directly with synthetic - // dimensions, no Window or GPU device needed at runtime. - Test clipTest; - Configuration& clc = clipTest.config; - clc.path = cfg.path; - clc.name = "UIClipFlag"; - clc.outputName = "UIClipFlag"; - clc.type = ConfigurationType::Executable; - clc.target = cfg.target; - clc.march = cfg.march; - clc.mtune = cfg.mtune; - clc.debug = cfg.debug; - clc.sysroot = cfg.sysroot; - clc.dependencies = cfg.dependencies; - clc.externalDependencies = cfg.externalDependencies; - clc.compileFlags = cfg.compileFlags; - clc.linkFlags = cfg.linkFlags; - clc.defines = cfg.defines; - clc.cFiles = cfg.cFiles; - std::vector clipImpls(impls.begin(), impls.end()); - clipImpls.emplace_back("tests/UIClipFlag/main"); - clc.GetInterfacesAndImplementations(ifaces, clipImpls); - cfg.tests.push_back(std::move(clipTest)); - // Issue #56: InputField_HitTestCursor mapped a click x to a cursor byte // offset by re-walking the prefix for every boundary (O(n^2) glyph // metric lookups) over raw byte boundaries. It now delegates to @@ -933,67 +443,6 @@ extern "C" Configuration CrafterBuildProject(std::span a hitImpls.emplace_back("tests/InputFieldHitTest/main"); hc.GetInterfacesAndImplementations(ifaces, hitImpls); cfg.tests.push_back(std::move(hitTest)); - - // Issue #128: DrawInputField re-measured the cursor prefix via - // Font::GetLineWidth every frame of a focused field even though only the - // blink changes frame-to-frame. The prefix WIDTH is now memoised on the - // InputField keyed on (prefix bytes, fontSize); the absolute caretX is - // deliberately not cached so a relocated field can't get a stale caret. - // The memo is transparent presentation logic over a TrueType file — - // DrawText no-ops with a null atlas/renderer — so this test drives - // DrawInputField directly with no Vulkan device or window. The font is - // copied next to the binary; the test also probes the project root. - Test caretTest; - Configuration& crc = caretTest.config; - crc.path = cfg.path; - crc.name = "InputFieldCaretCache"; - crc.outputName = "InputFieldCaretCache"; - crc.type = ConfigurationType::Executable; - crc.target = cfg.target; - crc.march = cfg.march; - crc.mtune = cfg.mtune; - crc.debug = cfg.debug; - crc.sysroot = cfg.sysroot; - crc.dependencies = cfg.dependencies; - crc.externalDependencies = cfg.externalDependencies; - crc.compileFlags = cfg.compileFlags; - crc.linkFlags = cfg.linkFlags; - crc.defines = cfg.defines; - crc.cFiles = cfg.cFiles; - crc.files = { fs::path("tests/InputFieldCaretCache/font.ttf") }; - std::vector caretImpls(impls.begin(), impls.end()); - caretImpls.emplace_back("tests/InputFieldCaretCache/main"); - crc.GetInterfacesAndImplementations(ifaces, caretImpls); - cfg.tests.push_back(std::move(caretTest)); - - // Issue #69: the engine feeds one shared Device::pipelineCache to every - // vkCreate*Pipelines call and persists it across runs, discarding an - // on-disk blob whose header doesn't match the current GPU. That gate — - // Device::PipelineCacheDataCompatible — is pure logic over the standard - // VkPipelineCache header and Device::deviceProperties, so this test - // stamps synthetic device identities + headers and drives it directly, - // no GPU device needed at runtime. - Test cacheTest; - Configuration& cc = cacheTest.config; - cc.path = cfg.path; - cc.name = "PipelineCacheValidation"; - cc.outputName = "PipelineCacheValidation"; - cc.type = ConfigurationType::Executable; - cc.target = cfg.target; - cc.march = cfg.march; - cc.mtune = cfg.mtune; - cc.debug = cfg.debug; - cc.sysroot = cfg.sysroot; - cc.dependencies = cfg.dependencies; - cc.externalDependencies = cfg.externalDependencies; - cc.compileFlags = cfg.compileFlags; - cc.linkFlags = cfg.linkFlags; - cc.defines = cfg.defines; - cc.cFiles = cfg.cFiles; - std::vector cacheImpls(impls.begin(), impls.end()); - cacheImpls.emplace_back("tests/PipelineCacheValidation/main"); - cc.GetInterfacesAndImplementations(ifaces, cacheImpls); - cfg.tests.push_back(std::move(cacheTest)); } return cfg; diff --git a/shaders/ui-circles.comp.glsl b/shaders/ui-circles.comp.glsl index 50b67a6..eea0c93 100644 --- a/shaders/ui-circles.comp.glsl +++ b/shaders/ui-circles.comp.glsl @@ -1,10 +1,5 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_GOOGLE_include_directive : enable -#extension GL_KHR_shader_subgroup_basic : enable -#extension GL_KHR_shader_subgroup_arithmetic : enable #include "ui-shared.glsl" // One workgroup per 8×8 screen tile. The workgroup cooperatively streams the @@ -23,49 +18,17 @@ shared vec4 s_outline[UI_CHUNK]; shared uint s_keep[UI_CHUNK]; shared uint s_order[UI_CHUNK]; shared uint s_count; -shared uint s_subTotals[UI_CHUNK]; // per-subgroup survivor totals (carry step) - -// Stable in-order compaction of one chunk's survivors via a per-subgroup -// exclusive prefix-sum plus a carry across subgroups, so a survivor's slot in -// s_order[] equals the number of survivors with a smaller local index — buffer -// (draw) order preserved. Replaces the old lane-0 serial scan. See -// ui-fused.comp.glsl for the full rationale; the body is identical. -void uiCompactChunk() { - uint lid = gl_LocalInvocationIndex; - uint keep = s_keep[lid]; - uint subPrefix = subgroupExclusiveAdd(keep); - uint subTotal = subgroupAdd(keep); - if (subgroupElect()) - s_subTotals[gl_SubgroupID] = subTotal; - barrier(); - - uint base = 0u; - uint total = 0u; - for (uint s = 0u; s < gl_NumSubgroups; ++s) { - uint t = s_subTotals[s]; - if (s < gl_SubgroupID) base += t; - total += t; - } - if (lid == 0u) s_count = total; - - if (keep != 0u) - s_order[base + subPrefix] = lid; -} void main() { ivec2 screenPx; bool valid = uiResolveScreenPixel(pc.hdr, screenPx); - // Defer the destination read-modify-write: a sparse UI leaves most tiles - // untouched, so only load the pixel when the first surviving item blends - // over it (`loaded`), and only store when something actually touched it. - // The fused kernel amortizes a single load/store across all categories; the - // standalone Dispatch* path has no such umbrella and otherwise pays a full - // read-modify-write per empty tile. - vec4 dst = vec4(0.0); - vec2 sp = vec2(0.0); - bool loaded = false; - if (valid) sp = vec2(screenPx) + 0.5; + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + if (valid) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + sp = vec2(screenPx) + 0.5; + } vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -89,7 +52,13 @@ void main() { s_keep[lid] = keep ? 1u : 0u; barrier(); - uiCompactChunk(); + if (lid == 0u) { + uint n = 0u; + uint lim = min(UI_CHUNK, pc.hdr.itemCount - base); + for (uint k = 0u; k < lim; ++k) + if (s_keep[k] != 0u) s_order[n++] = k; + s_count = n; + } barrier(); if (valid) { @@ -121,15 +90,11 @@ void main() { } if (src.a <= 0.0) continue; - if (!loaded) { - dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); - loaded = true; - } dst = uiBlendOver(dst, src); } } barrier(); } - if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid + if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); } diff --git a/shaders/ui-fused.comp.glsl b/shaders/ui-fused.comp.glsl deleted file mode 100644 index 0678dae..0000000 --- a/shaders/ui-fused.comp.glsl +++ /dev/null @@ -1,376 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -#version 460 -#extension GL_GOOGLE_include_directive : enable -#extension GL_KHR_shader_subgroup_basic : enable -#extension GL_KHR_shader_subgroup_arithmetic : enable -#include "ui-shared.glsl" - -// ─── fused UI uber-kernel (issue #47) ──────────────────────────────────── -// One dispatch that composites up to four standard categories in canonical -// back-to-front order — quads → circles → images → text — into a single -// per-pixel register, loading the destination image ONCE and storing it ONCE. -// A run of consecutive Dispatch* calls would instead load+store the image per -// category and drain compute with a VkMemoryBarrier between each; this kernel -// collapses all of that to 1 load + 1 store + 0 inter-pass barriers. -// -// Each category keeps the exact cooperative shared-memory tile-cull + per-pixel -// accumulate of its standalone shader (see ui-quads/circles/images/text), so a -// fused category is pixel-identical to its standalone Dispatch* pass (the only -// difference is that intermediate results stay in full float precision between -// categories instead of round-tripping through the storage image — strictly -// more accurate). Categories run sequentially within each thread, so the VGPR -// high-water mark is ~max(per-category), not the sum. -// -// An absent category (itemCount 0) is a zero-trip, push-constant-uniform loop: -// no divergence, no memory traffic — using DispatchFused for only quads+text -// costs ~nothing for the unused circle/image phases. -// -// This is ADDITIVE: it has its own push-constant layout and does NOT touch the -// frozen 48-byte UIDispatchHeader or the per-element Dispatch* contract. - -// Push-constant block. Mirrors Crafter::UIFusedHeader byte-for-byte: every -// member is vec4-aligned, no padding holes, exactly 128 bytes (the guaranteed -// push-constant minimum). -layout(push_constant) uniform PC { - uvec4 itemBuffers; // heap slots: (quads, circles, images, text) - uvec4 itemCounts; // item counts: (quads, circles, images, text) - uvec4 misc; // (outImage, fontTexture, fontSampler, flags) - uvec4 surface; // (surfaceWidth, surfaceHeight, frameIdx, _pad) - vec4 clipQuads; - vec4 clipCircles; - vec4 clipImages; - vec4 clipText; -} pc; - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; - -// SDF tuning for the text phase — must match Crafter::FontAtlas constants -// (same values as ui-text.comp.glsl). -const float ON_EDGE = 128.0 / 255.0; -const float DIST_SCALE = 32.0; - -// Per-category clip-active bits packed into pc.misc.w (the fused analogue of -// ui-shared.glsl's UI_FLAG_CLIP). A bit is set only when that category's clip -// rect is narrower than the surface; when clear, the category skips its four -// per-pixel clip compares entirely (the common full-surface case). The branch -// is push-constant-uniform, so it never diverges. -const uint UI_FUSED_CLIP_QUADS = 0x1u; -const uint UI_FUSED_CLIP_CIRCLES = 0x2u; -const uint UI_FUSED_CLIP_IMAGES = 0x4u; -const uint UI_FUSED_CLIP_TEXT = 0x8u; - -// Generic per-chunk cooperative-cull scratch, REUSED across the four phases -// (they composite sequentially with a barrier between, so the storage is free -// once a phase's last read completes). Keeping one shared set instead of four -// keeps the shared-memory footprint — and thus occupancy — at the per-category -// level. Member mapping per phase: -// quads: v0=rect v1=color v2=corners v3=outline -// circles: v0=centerRadius v1=color v2=outline -// images: v0=rect v1=uv v2=tint v4=slots -// text: v0=rect v1=uv v2=color -shared vec4 s_v0[UI_CHUNK]; -shared vec4 s_v1[UI_CHUNK]; -shared vec4 s_v2[UI_CHUNK]; -shared vec4 s_v3[UI_CHUNK]; -shared uvec4 s_v4[UI_CHUNK]; -// Per-item constants precomputed once at cooperative-load time, REUSED across -// phases like the s_v* scratch (the per-pixel inner loops read them instead of -// recomputing per pixel × item): -// images: s_inv = 1.0/rect.zw -// text: s_inv = 1.0/rect.zw, s_band = SDF AA scale (a divide + two maxes) -shared vec2 s_inv[UI_CHUNK]; -shared float s_band[UI_CHUNK]; -shared uint s_keep[UI_CHUNK]; -shared uint s_order[UI_CHUNK]; -shared uint s_count; -// Per-subgroup survivor totals, published once per subgroup for the carry step -// of the compaction below. Sized for the worst case of one subgroup per lane. -shared uint s_subTotals[UI_CHUNK]; - -// Stable in-order compaction of one chunk's survivors. Each subgroup computes -// an exclusive prefix-sum of its keep bits at register speed (subgroupExclusiveAdd), -// publishes its survivor total to shared memory, then every lane carries in the -// totals of all lower-id subgroups. A survivor's slot in s_order[] therefore -// equals the number of survivors with a smaller local index, so the per-pixel -// inner loop still sees items in buffer (draw) order. This replaces the old -// lane-0 serial scan (~64 iterations with 63/64 lanes idle, run 4× per chunk). -// -// Order preservation relies on the gl_LocalInvocationIndex ↔ (gl_SubgroupID, -// gl_SubgroupInvocationID) linear mapping that every Vulkan compute -// implementation provides; the carry across subgroups makes the result correct -// for any subgroup width (2 subgroups on the 32-wide descriptor_heap target, up -// to 8 on 8-wide parts). Out-of-range lanes carry s_keep==0, so they neither -// add to a total nor write a slot — no separate bound is needed. -void uiCompactChunk() { - uint lid = gl_LocalInvocationIndex; - uint keep = s_keep[lid]; - uint subPrefix = subgroupExclusiveAdd(keep); // rank within this subgroup - uint subTotal = subgroupAdd(keep); // survivors in this subgroup - if (subgroupElect()) - s_subTotals[gl_SubgroupID] = subTotal; - barrier(); - - uint base = 0u; - uint total = 0u; - for (uint s = 0u; s < gl_NumSubgroups; ++s) { - uint t = s_subTotals[s]; - if (s < gl_SubgroupID) base += t; // survivors in lower subgroups - total += t; - } - if (lid == 0u) s_count = total; - - if (keep != 0u) - s_order[base + subPrefix] = lid; -} - -void main() { - // NOTE: do not early-return — every thread must reach the barriers below. - uvec2 pxu = gl_GlobalInvocationID.xy; - bool inSurface = pxu.x < pc.surface.x && pxu.y < pc.surface.y; - ivec2 screenPx = ivec2(pxu); - vec2 sp = vec2(screenPx) + 0.5; - - // Single load of the destination for the whole surface. - vec4 dst = vec4(0.0); - if (inSurface) dst = imageLoad(uiImages[pc.misc.x], screenPx); - - vec2 tileMin, tileMax; - uiTileBounds(tileMin, tileMax); - uint lid = gl_LocalInvocationIndex; - - // ─── QUADS ──────────────────────────────────────────────────────────── - { - uint heap = pc.itemBuffers.x; - uint count = pc.itemCounts.x; - bool inClip = inSurface && - ((pc.misc.w & UI_FUSED_CLIP_QUADS) == 0u || uiPixelInClipRect(pxu, pc.clipQuads)); - for (uint base = 0u; base < count; base += UI_CHUNK) { - uint idx = base + lid; - bool keep = false; - if (idx < count) { - s_v0[lid] = uiQuadHeap[heap].items[idx].rect; - s_v1[lid] = uiQuadHeap[heap].items[idx].color; - s_v2[lid] = uiQuadHeap[heap].items[idx].corners; - s_v3[lid] = uiQuadHeap[heap].items[idx].outline; - keep = uiAabbOverlapsTile(s_v0[lid].xy, s_v0[lid].xy + s_v0[lid].zw, - tileMin, tileMax); - } - s_keep[lid] = keep ? 1u : 0u; - barrier(); - uiCompactChunk(); - barrier(); - - if (inClip) { - for (uint j = 0u; j < s_count; ++j) { - uint c = s_order[j]; - - vec2 lo = s_v0[c].xy; - vec2 hi = s_v0[c].xy + s_v0[c].zw; - if (sp.x < lo.x || sp.y < lo.y) continue; - if (sp.x >= hi.x || sp.y >= hi.y) continue; - - vec2 halfSize = s_v0[c].zw * 0.5; - vec2 p = sp - (s_v0[c].xy + halfSize); - float d = uiSdRoundRect(p, halfSize, s_v2[c]); - - vec4 outline = s_v3[c]; - float bodyA = clamp(0.5 - d, 0.0, 1.0); - if (bodyA <= 0.0 && outline.x <= 0.0) continue; - - vec4 col = s_v1[c]; - vec4 src = vec4(col.rgb, col.a * bodyA); - - if (outline.x > 0.0) { - float t = abs(d + outline.x * 0.5) - outline.x * 0.5; - float outlineA = clamp(0.5 - t, 0.0, 1.0); - src.rgb = mix(src.rgb, outline.yzw, outlineA); - src.a = max(src.a, outlineA); - } - - if (src.a <= 0.0) continue; - dst = uiBlendOver(dst, src); - } - } - barrier(); // done reading shared for this chunk before reuse - } - } - - // ─── CIRCLES ────────────────────────────────────────────────────────── - { - uint heap = pc.itemBuffers.y; - uint count = pc.itemCounts.y; - bool inClip = inSurface && - ((pc.misc.w & UI_FUSED_CLIP_CIRCLES) == 0u || uiPixelInClipRect(pxu, pc.clipCircles)); - for (uint base = 0u; base < count; base += UI_CHUNK) { - uint idx = base + lid; - bool keep = false; - if (idx < count) { - s_v0[lid] = uiCircleHeap[heap].items[idx].centerRadius; - s_v1[lid] = uiCircleHeap[heap].items[idx].color; - s_v2[lid] = uiCircleHeap[heap].items[idx].outline; - float radius = s_v0[lid].z; - if (radius > 0.0) { - vec2 cen = s_v0[lid].xy; - vec2 r = vec2(radius + 1.0); - keep = uiAabbOverlapsTile(cen - r, cen + r, tileMin, tileMax); - } - } - s_keep[lid] = keep ? 1u : 0u; - barrier(); - uiCompactChunk(); - barrier(); - - if (inClip) { - for (uint j = 0u; j < s_count; ++j) { - uint c = s_order[j]; - - vec2 center = s_v0[c].xy; - float radius = s_v0[c].z; - if (radius <= 0.0) continue; - - if (abs(sp.x - center.x) > radius + 1.0) continue; - if (abs(sp.y - center.y) > radius + 1.0) continue; - - float d = length(sp - center) - radius; - - vec4 outline = s_v2[c]; - float bodyA = clamp(0.5 - d, 0.0, 1.0); - if (bodyA <= 0.0 && outline.x <= 0.0) continue; - - vec4 col = s_v1[c]; - vec4 src = vec4(col.rgb, col.a * bodyA); - - if (outline.x > 0.0) { - float t = abs(d + outline.x * 0.5) - outline.x * 0.5; - float outlineA = clamp(0.5 - t, 0.0, 1.0); - src.rgb = mix(src.rgb, outline.yzw, outlineA); - src.a = max(src.a, outlineA); - } - - if (src.a <= 0.0) continue; - dst = uiBlendOver(dst, src); - } - } - barrier(); - } - } - - // ─── IMAGES ─────────────────────────────────────────────────────────── - { - uint heap = pc.itemBuffers.z; - uint count = pc.itemCounts.z; - bool inClip = inSurface && - ((pc.misc.w & UI_FUSED_CLIP_IMAGES) == 0u || uiPixelInClipRect(pxu, pc.clipImages)); - for (uint base = 0u; base < count; base += UI_CHUNK) { - uint idx = base + lid; - bool keep = false; - if (idx < count) { - s_v0[lid] = uiImageHeap[heap].items[idx].rect; - s_v1[lid] = uiImageHeap[heap].items[idx].uv; - s_v2[lid] = uiImageHeap[heap].items[idx].tint; - s_v4[lid] = uiImageHeap[heap].items[idx].slots; - s_inv[lid] = 1.0 / s_v0[lid].zw; - keep = uiAabbOverlapsTile(s_v0[lid].xy, s_v0[lid].xy + s_v0[lid].zw, - tileMin, tileMax); - } - s_keep[lid] = keep ? 1u : 0u; - barrier(); - uiCompactChunk(); - barrier(); - - if (inClip) { - for (uint j = 0u; j < s_count; ++j) { - uint c = s_order[j]; - - vec2 lo = s_v0[c].xy; - vec2 hi = s_v0[c].xy + s_v0[c].zw; - if (sp.x < lo.x || sp.y < lo.y) continue; - if (sp.x >= hi.x || sp.y >= hi.y) continue; - - vec2 t = (sp - s_v0[c].xy) * s_inv[c]; - vec2 uv = mix(s_v1[c].xy, s_v1[c].zw, t); - - uint texSlot = s_v4[c].x; - uint sampSlot = s_v4[c].y; - - vec4 sampled = texture( - sampler2D(uiTextures[nonuniformEXT(texSlot)], - uiSamplers[nonuniformEXT(sampSlot)]), - uv - ); - vec4 src = sampled * s_v2[c]; - if (src.a <= 0.0) continue; - dst = uiBlendOver(dst, src); - } - } - barrier(); - } - } - - // ─── TEXT ───────────────────────────────────────────────────────────── - { - uint heap = pc.itemBuffers.w; - uint count = pc.itemCounts.w; - bool inClip = inSurface && - ((pc.misc.w & UI_FUSED_CLIP_TEXT) == 0u || uiPixelInClipRect(pxu, pc.clipText)); - for (uint base = 0u; base < count; base += UI_CHUNK) { - uint idx = base + lid; - bool keep = false; - if (idx < count) { - s_v0[lid] = uiGlyphHeap[heap].items[idx].rect; - s_v1[lid] = uiGlyphHeap[heap].items[idx].uv; - s_v2[lid] = uiGlyphHeap[heap].items[idx].color; - s_inv[lid] = 1.0 / s_v0[lid].zw; - // SDF AA band — atlas-px per screen-px, constant per glyph - // (uvSpan * kAtlasSize(1024) / screenSpan), max'd to 1px floor. - vec2 uvSpan = s_v1[lid].zw - s_v1[lid].xy; - vec2 atlasPerScreen = (uvSpan * 1024.0) * s_inv[lid]; - float scalePx = max(atlasPerScreen.x, atlasPerScreen.y); - s_band[lid] = max(scalePx, 0.0001); - keep = uiAabbOverlapsTile(s_v0[lid].xy, s_v0[lid].xy + s_v0[lid].zw, - tileMin, tileMax); - } - s_keep[lid] = keep ? 1u : 0u; - barrier(); - uiCompactChunk(); - barrier(); - - if (inClip) { - for (uint j = 0u; j < s_count; ++j) { - uint c = s_order[j]; - - vec2 lo = s_v0[c].xy; - vec2 hi = s_v0[c].xy + s_v0[c].zw; - if (sp.x < lo.x || sp.y < lo.y) continue; - if (sp.x >= hi.x || sp.y >= hi.y) continue; - - vec2 t = (sp - s_v0[c].xy) * s_inv[c]; - vec2 uv = mix(s_v1[c].xy, s_v1[c].zw, t); - - // Font slots are push constants — provably dynamically uniform, - // so no nonuniformEXT (same as ui-text.comp.glsl). - float sdf = texture( - sampler2D(uiTextures[pc.misc.y], - uiSamplers[pc.misc.z]), - uv - ).r; - - float dAtlas = (ON_EDGE - sdf) * DIST_SCALE; - - // band (the AA scale) was precomputed once per glyph at load. - float a = clamp(0.5 - dAtlas / s_band[c], 0.0, 1.0); - if (a <= 0.0) continue; - - vec4 col = s_v2[c]; - vec4 src = vec4(col.rgb, col.a * a); - dst = uiBlendOver(dst, src); - } - } - barrier(); - } - } - - if (inSurface) imageStore(uiImages[pc.misc.x], screenPx, dst); -} diff --git a/shaders/ui-images.comp.glsl b/shaders/ui-images.comp.glsl index 01715c0..a4424a6 100644 --- a/shaders/ui-images.comp.glsl +++ b/shaders/ui-images.comp.glsl @@ -1,10 +1,5 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_GOOGLE_include_directive : enable -#extension GL_KHR_shader_subgroup_basic : enable -#extension GL_KHR_shader_subgroup_arithmetic : enable #include "ui-shared.glsl" // One workgroup per 8×8 screen tile. The workgroup cooperatively streams the @@ -21,57 +16,20 @@ shared vec4 s_rect[UI_CHUNK]; shared vec4 s_uv[UI_CHUNK]; shared vec4 s_tint[UI_CHUNK]; shared uvec4 s_slots[UI_CHUNK]; -// 1.0/rect.zw, precomputed once per item at cooperative-load time so the -// per-pixel inner loop multiplies instead of recomputing a vec2 reciprocal of -// the (loop-invariant, but shared-mem + varying-index, so non-hoistable by the -// compiler) rect size for every pixel × item. -shared vec2 s_invRectSize[UI_CHUNK]; shared uint s_keep[UI_CHUNK]; shared uint s_order[UI_CHUNK]; shared uint s_count; -shared uint s_subTotals[UI_CHUNK]; // per-subgroup survivor totals (carry step) - -// Stable in-order compaction of one chunk's survivors via a per-subgroup -// exclusive prefix-sum plus a carry across subgroups, so a survivor's slot in -// s_order[] equals the number of survivors with a smaller local index — buffer -// (draw) order preserved. Replaces the old lane-0 serial scan. See -// ui-fused.comp.glsl for the full rationale; the body is identical. -void uiCompactChunk() { - uint lid = gl_LocalInvocationIndex; - uint keep = s_keep[lid]; - uint subPrefix = subgroupExclusiveAdd(keep); - uint subTotal = subgroupAdd(keep); - if (subgroupElect()) - s_subTotals[gl_SubgroupID] = subTotal; - barrier(); - - uint base = 0u; - uint total = 0u; - for (uint s = 0u; s < gl_NumSubgroups; ++s) { - uint t = s_subTotals[s]; - if (s < gl_SubgroupID) base += t; - total += t; - } - if (lid == 0u) s_count = total; - - if (keep != 0u) - s_order[base + subPrefix] = lid; -} void main() { ivec2 screenPx; bool valid = uiResolveScreenPixel(pc.hdr, screenPx); - // Defer the destination read-modify-write: a sparse UI leaves most tiles - // untouched, so only load the pixel when the first surviving item blends - // over it (`loaded`), and only store when something actually touched it. - // The fused kernel amortizes a single load/store across all categories; the - // standalone Dispatch* path has no such umbrella and otherwise pays a full - // read-modify-write per empty tile. - vec4 dst = vec4(0.0); - vec2 sp = vec2(0.0); - bool loaded = false; - if (valid) sp = vec2(screenPx) + 0.5; + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + if (valid) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + sp = vec2(screenPx) + 0.5; + } vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -85,14 +43,19 @@ void main() { s_uv[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].uv; s_tint[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].tint; s_slots[lid] = uiImageHeap[pc.hdr.itemBuffer].items[idx].slots; - s_invRectSize[lid] = 1.0 / s_rect[lid].zw; keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw, tileMin, tileMax); } s_keep[lid] = keep ? 1u : 0u; barrier(); - uiCompactChunk(); + if (lid == 0u) { + uint n = 0u; + uint lim = min(UI_CHUNK, pc.hdr.itemCount - base); + for (uint k = 0u; k < lim; ++k) + if (s_keep[k] != 0u) s_order[n++] = k; + s_count = n; + } barrier(); if (valid) { @@ -104,7 +67,7 @@ void main() { if (sp.x < lo.x || sp.y < lo.y) continue; if (sp.x >= hi.x || sp.y >= hi.y) continue; - vec2 t = (sp - s_rect[c].xy) * s_invRectSize[c]; + vec2 t = (sp - s_rect[c].xy) / s_rect[c].zw; vec2 uv = mix(s_uv[c].xy, s_uv[c].zw, t); uint texSlot = s_slots[c].x; @@ -117,15 +80,11 @@ void main() { ); vec4 src = sampled * s_tint[c]; if (src.a <= 0.0) continue; - if (!loaded) { - dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); - loaded = true; - } dst = uiBlendOver(dst, src); } } barrier(); } - if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid + if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); } diff --git a/shaders/ui-quads.comp.glsl b/shaders/ui-quads.comp.glsl index 5fed2ee..24e7daf 100644 --- a/shaders/ui-quads.comp.glsl +++ b/shaders/ui-quads.comp.glsl @@ -1,10 +1,5 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_GOOGLE_include_directive : enable -#extension GL_KHR_shader_subgroup_basic : enable -#extension GL_KHR_shader_subgroup_arithmetic : enable #include "ui-shared.glsl" // One workgroup per 8×8 screen tile. The workgroup cooperatively streams the @@ -26,50 +21,18 @@ shared vec4 s_outline[UI_CHUNK]; shared uint s_keep[UI_CHUNK]; shared uint s_order[UI_CHUNK]; shared uint s_count; -shared uint s_subTotals[UI_CHUNK]; // per-subgroup survivor totals (carry step) - -// Stable in-order compaction of one chunk's survivors via a per-subgroup -// exclusive prefix-sum plus a carry across subgroups, so a survivor's slot in -// s_order[] equals the number of survivors with a smaller local index — buffer -// (draw) order preserved. Replaces the old lane-0 serial scan. See -// ui-fused.comp.glsl for the full rationale; the body is identical. -void uiCompactChunk() { - uint lid = gl_LocalInvocationIndex; - uint keep = s_keep[lid]; - uint subPrefix = subgroupExclusiveAdd(keep); - uint subTotal = subgroupAdd(keep); - if (subgroupElect()) - s_subTotals[gl_SubgroupID] = subTotal; - barrier(); - - uint base = 0u; - uint total = 0u; - for (uint s = 0u; s < gl_NumSubgroups; ++s) { - uint t = s_subTotals[s]; - if (s < gl_SubgroupID) base += t; - total += t; - } - if (lid == 0u) s_count = total; - - if (keep != 0u) - s_order[base + subPrefix] = lid; -} void main() { // NOTE: do not early-return — every thread must reach the barriers below. ivec2 screenPx; bool valid = uiResolveScreenPixel(pc.hdr, screenPx); - // Defer the destination read-modify-write: a sparse UI leaves most tiles - // untouched, so only load the pixel when the first surviving item blends - // over it (`loaded`), and only store when something actually touched it. - // The fused kernel amortizes a single load/store across all categories; the - // standalone Dispatch* path has no such umbrella and otherwise pays a full - // read-modify-write per empty tile. - vec4 dst = vec4(0.0); - vec2 sp = vec2(0.0); - bool loaded = false; - if (valid) sp = vec2(screenPx) + 0.5; + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + if (valid) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + sp = vec2(screenPx) + 0.5; + } vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -91,7 +54,14 @@ void main() { s_keep[lid] = keep ? 1u : 0u; barrier(); - uiCompactChunk(); + // Stable in-order compaction of this chunk's survivors. + if (lid == 0u) { + uint n = 0u; + uint lim = min(UI_CHUNK, pc.hdr.itemCount - base); + for (uint k = 0u; k < lim; ++k) + if (s_keep[k] != 0u) s_order[n++] = k; + s_count = n; + } barrier(); if (valid) { @@ -123,15 +93,11 @@ void main() { } if (src.a <= 0.0) continue; - if (!loaded) { - dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); - loaded = true; - } dst = uiBlendOver(dst, src); } } barrier(); // done reading shared for this chunk before it's overwritten } - if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid + if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); } diff --git a/shaders/ui-shared.glsl b/shaders/ui-shared.glsl index b06888b..52e29b3 100644 --- a/shaders/ui-shared.glsl +++ b/shaders/ui-shared.glsl @@ -1,6 +1,3 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - // Crafter.Graphics UI shader contract — shared by every standard UI compute // shader and intended to be #included by user-authored shaders that want to // dispatch alongside them. Layouts here are FROZEN: only additive changes @@ -30,19 +27,10 @@ struct UIDispatchHeader { vec4 clipRectPx; // (xy, wh) — every standard shader honors this uint itemCount; uint frameIdx; - uint flags; // feature bits — high bit reserved by the renderer + uint flags; // user-defined feature bits uint _pad; // reserved — keep zeroed }; -// ─── header flag bits ─────────────────────────────────────────────────── -// `flags` is otherwise free for user-defined feature bits, but the renderer -// reserves the high bit: UI_FLAG_CLIP is set by UIRenderer::FillHeader when -// the clip rect does not already cover the whole surface. uiResolveScreenPixel -// gates its four clipRectPx compares on this bit so the overwhelmingly common -// full-surface case (clipRect == {0,0,1e9,1e9}) skips them entirely. The -// branch is push-constant-uniform across the dispatch, so it never diverges. -const uint UI_FLAG_CLIP = 0x80000000u; - // ─── standard item structs ────────────────────────────────────────────── // These match the C++ Crafter::QuadItem / CircleItem / ImageItem / GlyphItem // byte-for-byte under std430. @@ -133,28 +121,13 @@ QuadItem LoadQuadItem(uint heap, uint i) { bool uiResolveScreenPixel(UIDispatchHeader hdr, out ivec2 screenPx) { uvec2 px = gl_GlobalInvocationID.xy; if (px.x >= hdr.surfaceSize.x || px.y >= hdr.surfaceSize.y) return false; - if ((hdr.flags & UI_FLAG_CLIP) != 0u) { - if (float(px.x) < hdr.clipRectPx.x || float(px.y) < hdr.clipRectPx.y) return false; - if (float(px.x) >= hdr.clipRectPx.x + hdr.clipRectPx.z) return false; - if (float(px.y) >= hdr.clipRectPx.y + hdr.clipRectPx.w) return false; - } + if (float(px.x) < hdr.clipRectPx.x || float(px.y) < hdr.clipRectPx.y) return false; + if (float(px.x) >= hdr.clipRectPx.x + hdr.clipRectPx.z) return false; + if (float(px.y) >= hdr.clipRectPx.y + hdr.clipRectPx.w) return false; screenPx = ivec2(px); return true; } -// Per-category clip test for the fused kernel (issue #47). The fused shader -// loads/stores the destination image once for the whole surface but each -// category carries its own clip rect, so the clip can only gate compositing — -// not the shared load/store. This mirrors the integer-pixel comparison -// uiResolveScreenPixel does (compare the pixel's top-left, not its center), so -// a fused category is pixel-identical to its standalone Dispatch* pass. -bool uiPixelInClipRect(uvec2 px, vec4 clip) { - if (float(px.x) < clip.x || float(px.y) < clip.y) return false; - if (float(px.x) >= clip.x + clip.z) return false; - if (float(px.y) >= clip.y + clip.w) return false; - return true; -} - // Non-premultiplied "src over dst" blend. Both operands and result are // straight-alpha vec4. Use this when iterating items in a loop with a local // accumulator. diff --git a/shaders/ui-text.comp.glsl b/shaders/ui-text.comp.glsl index 1b7afa9..a64abbf 100644 --- a/shaders/ui-text.comp.glsl +++ b/shaders/ui-text.comp.glsl @@ -1,10 +1,5 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - #version 460 #extension GL_GOOGLE_include_directive : enable -#extension GL_KHR_shader_subgroup_basic : enable -#extension GL_KHR_shader_subgroup_arithmetic : enable #include "ui-shared.glsl" // One workgroup per 8×8 screen tile. The workgroup cooperatively streams the @@ -28,59 +23,20 @@ const float DIST_SCALE = 32.0; shared vec4 s_rect[UI_CHUNK]; shared vec4 s_uv[UI_CHUNK]; shared vec4 s_color[UI_CHUNK]; -// Per-glyph constants precomputed once at cooperative-load time and read by the -// per-pixel inner loop, instead of being recomputed for every pixel × glyph: -// s_invRectSize = 1.0/rect.zw (turns the per-pixel divide into a multiply) -// s_band = the SDF AA scale (a divide + two maxes that depend only on -// the glyph's uv span and rect size — fully loop-invariant) -shared vec2 s_invRectSize[UI_CHUNK]; -shared float s_band[UI_CHUNK]; shared uint s_keep[UI_CHUNK]; shared uint s_order[UI_CHUNK]; shared uint s_count; -shared uint s_subTotals[UI_CHUNK]; // per-subgroup survivor totals (carry step) - -// Stable in-order compaction of one chunk's survivors via a per-subgroup -// exclusive prefix-sum plus a carry across subgroups, so a survivor's slot in -// s_order[] equals the number of survivors with a smaller local index — buffer -// (draw) order preserved. Replaces the old lane-0 serial scan. See -// ui-fused.comp.glsl for the full rationale; the body is identical. -void uiCompactChunk() { - uint lid = gl_LocalInvocationIndex; - uint keep = s_keep[lid]; - uint subPrefix = subgroupExclusiveAdd(keep); - uint subTotal = subgroupAdd(keep); - if (subgroupElect()) - s_subTotals[gl_SubgroupID] = subTotal; - barrier(); - - uint base = 0u; - uint total = 0u; - for (uint s = 0u; s < gl_NumSubgroups; ++s) { - uint t = s_subTotals[s]; - if (s < gl_SubgroupID) base += t; - total += t; - } - if (lid == 0u) s_count = total; - - if (keep != 0u) - s_order[base + subPrefix] = lid; -} void main() { ivec2 screenPx; bool valid = uiResolveScreenPixel(pc.hdr, screenPx); - // Defer the destination read-modify-write: a sparse UI leaves most tiles - // untouched, so only load the pixel when the first surviving glyph blends - // over it (`loaded`), and only store when something actually touched it. - // The fused kernel amortizes a single load/store across all categories; the - // standalone Dispatch* path has no such umbrella and otherwise pays a full - // read-modify-write per empty tile. - vec4 dst = vec4(0.0); - vec2 sp = vec2(0.0); - bool loaded = false; - if (valid) sp = vec2(screenPx) + 0.5; + vec4 dst = vec4(0.0); + vec2 sp = vec2(0.0); + if (valid) { + dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); + sp = vec2(screenPx) + 0.5; + } vec2 tileMin, tileMax; uiTileBounds(tileMin, tileMax); @@ -93,22 +49,19 @@ void main() { s_rect[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].rect; s_uv[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].uv; s_color[lid] = uiGlyphHeap[pc.hdr.itemBuffer].items[idx].color; - s_invRectSize[lid] = 1.0 / s_rect[lid].zw; - // Atlas-px per screen-px along this glyph's transform — keeps AA - // crisp at any rendering size. uvSpan * atlasSize / screenSpan, - // with FontAtlas::kAtlasSize = 1024. Constant for the whole glyph. - vec2 uvSpan = s_uv[lid].zw - s_uv[lid].xy; - vec2 atlasPerScreen = (uvSpan * 1024.0) * s_invRectSize[lid]; - float scalePx = max(atlasPerScreen.x, atlasPerScreen.y); - // 1-screen-px AA band, expressed in atlas-pixel units of dAtlas. - s_band[lid] = max(scalePx, 0.0001); keep = uiAabbOverlapsTile(s_rect[lid].xy, s_rect[lid].xy + s_rect[lid].zw, tileMin, tileMax); } s_keep[lid] = keep ? 1u : 0u; barrier(); - uiCompactChunk(); + if (lid == 0u) { + uint n = 0u; + uint lim = min(UI_CHUNK, pc.hdr.itemCount - base); + for (uint k = 0u; k < lim; ++k) + if (s_keep[k] != 0u) s_order[n++] = k; + s_count = n; + } barrier(); if (valid) { @@ -120,7 +73,7 @@ void main() { if (sp.x < lo.x || sp.y < lo.y) continue; if (sp.x >= hi.x || sp.y >= hi.y) continue; - vec2 t = (sp - s_rect[c].xy) * s_invRectSize[c]; + vec2 t = (sp - s_rect[c].xy) / s_rect[c].zw; vec2 uv = mix(s_uv[c].xy, s_uv[c].zw, t); // Font slots are push constants — provably dynamically uniform, so no @@ -135,21 +88,25 @@ void main() { // Distance in atlas-pixels (negative inside the glyph). float dAtlas = (ON_EDGE - sdf) * DIST_SCALE; - // band (the AA scale) was precomputed once per glyph at load. - float a = clamp(0.5 - dAtlas / s_band[c], 0.0, 1.0); + // Atlas-px per screen-px along this glyph's transform — keeps AA crisp + // at any rendering size. uvSpan * atlasSize / screenSpan. + vec2 uvSpan = s_uv[c].zw - s_uv[c].xy; + // FontAtlas::kAtlasSize = 1024. + vec2 atlasPerScreen = (uvSpan * 1024.0) / s_rect[c].zw; + float scalePx = max(atlasPerScreen.x, atlasPerScreen.y); + // 1-screen-px AA band, expressed in atlas-pixel units of dAtlas. + float band = max(scalePx, 0.0001); + + float a = clamp(0.5 - dAtlas / band, 0.0, 1.0); if (a <= 0.0) continue; vec4 col = s_color[c]; vec4 src = vec4(col.rgb, col.a * a); - if (!loaded) { - dst = imageLoad(uiImages[pc.hdr.outImage], screenPx); - loaded = true; - } dst = uiBlendOver(dst, src); } } barrier(); } - if (loaded) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); // loaded ⇒ valid + if (valid) imageStore(uiImages[pc.hdr.outImage], screenPx, dst); } diff --git a/tests/BLASBuildOptions/main.cpp b/tests/BLASBuildOptions/main.cpp index d8d70ae..f4fed5f 100644 --- a/tests/BLASBuildOptions/main.cpp +++ b/tests/BLASBuildOptions/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Issue #36: BLAS build options — fast-build / fast-trace preference and // in-place refit (UPDATE-mode rebuild). This exercises the real hardware @@ -112,19 +128,6 @@ int main() { Check((tri.buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR) != 0, "allowUpdate=true → ALLOW_UPDATE bit set"); Check(tri.builtPrimitiveCount == 12, "triangle BLAS reports 12 primitives"); - // allowUpdate=true keeps the scratch buffer alive so the in-place UPDATE - // refit below can reuse it (build scratch ≥ update scratch, no resize). - Check(tri.scratchBuffer.buffer != VK_NULL_HANDLE, - "allowUpdate=true → scratch retained for refit (issue #66)"); - // Geometry is placed in device-local memory regardless of the upload - // strategy taken (issue #73): the direct ReBAR/UMA path lands a - // HOST_VISIBLE | DEVICE_LOCAL type, the staged path a pure DEVICE_LOCAL - // one — either way the DEVICE_LOCAL bit is set, so the BLAS build and any - // hit-shader fetch read from VRAM, not system RAM. - Check((tri.vertexBuffer.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0, - "triangle vertexBuffer placed in device-local memory (#73)"); - Check((tri.indexBuffer.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0, - "triangle indexBuffer placed in device-local memory (#73)"); const VkDeviceAddress triAddrBefore = tri.blasAddr; const VkAccelerationStructureKHR triHandleBefore = tri.accelerationStructure; @@ -141,42 +144,6 @@ int main() { Check(tri.blasAddr == triAddrBefore, "Refit kept the same blasAddr (instances stay valid)"); - // Dirty-range refit (issue #119): move only a sub-window of the vertices - // and pass it as [offset, count). The ranged upload patches just those - // vertices in place; the UPDATE must still keep the same AS handle / - // blasAddr, and the validation layer (checked at the end) is the real proof - // that the ranged host-write / flush / barrier — and the staged variant - // below — are spec-correct. The untouched vertices retain their last values - // in the device buffer, so the refit BLAS is over the full deformed cube. - { - // Full array of moved positions; we declare only the back half [4,8) - // as the dirty window, so only those 4 vertices are re-uploaded. - auto verts = CubeVerts(2.0f); - auto idx = CubeIndices(); - VkCommandBuffer cmd = BeginCmd(); - tri.Refit(verts, idx, /*dirtyVertexOffset*/ 4, /*dirtyVertexCount*/ 4, cmd); - SubmitWait(cmd); - } - Check(tri.accelerationStructure == triHandleBefore, - "dirty-range Refit kept the same AS handle (in-place UPDATE, #119)"); - Check(tri.blasAddr == triAddrBefore, - "dirty-range Refit kept the same blasAddr (#119)"); - - // A dirty-range refit on a mesh whose counts changed must still fall back - // to a full rebuild (the dirty window is irrelevant on that path) and - // succeed — same robustness as the full-span Refit fallback. - { - auto verts = CubeVerts(1.0f); - verts.push_back({2.0f, 2.0f, 2.0f}); // count change → topology change - auto idx = CubeIndices(); - idx.insert(idx.end(), {0u, 1u, 8u}); - VkCommandBuffer cmd = BeginCmd(); - tri.Refit(verts, idx, /*dirtyVertexOffset*/ 8, /*dirtyVertexCount*/ 1, cmd); - SubmitWait(cmd); - } - Check(tri.blasAddr != 0 && tri.builtPrimitiveCount == 13, - "dirty-range Refit with a count change rebuilt the BLAS (13 prims, #119)"); - // ── Triangle BLAS: fast-build, no update → flags reflect the choice. ─ Mesh triFast; { @@ -190,12 +157,6 @@ int main() { "FastBuild preference → PREFER_FAST_BUILD bit set"); Check((triFast.buildFlags & VK_BUILD_ACCELERATION_STRUCTURE_ALLOW_UPDATE_BIT_KHR) == 0, "allowUpdate=false → ALLOW_UPDATE bit clear"); - // A static mesh can never refit, so its scratch is released right after - // the build completes rather than kept as persistent VRAM (issue #66). - // DeferredClear nulls the handle immediately (the allocation itself is - // retired by the fence-keyed queue once the build's frame passes). - Check(triFast.scratchBuffer.buffer == VK_NULL_HANDLE, - "allowUpdate=false → scratch released after build (issue #66)"); // Refit without allowUpdate must still succeed via the rebuild fallback. { @@ -206,10 +167,6 @@ int main() { SubmitWait(cmd); } Check(triFast.blasAddr != 0, "Refit fallback rebuild still produced a valid BLAS"); - // The fallback rebuild re-Created the scratch (Resize saw the nulled - // handle) and, still being a static mesh, released it again afterwards. - Check(triFast.scratchBuffer.buffer == VK_NULL_HANDLE, - "static-mesh refit rebuild re-released its scratch (issue #66)"); // ── Procedural (AABB) BLAS: build with options, then refit. ───────── Mesh proc; @@ -225,10 +182,6 @@ int main() { } Check(proc.blasAddr != 0, "procedural Build produced a non-zero blasAddr"); Check(proc.builtPrimitiveCount == 2, "procedural BLAS reports 2 primitives"); - Check(proc.scratchBuffer.buffer != VK_NULL_HANDLE, - "procedural allowUpdate=true → scratch retained for refit (issue #66)"); - Check((proc.aabbBuffer.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0, - "procedural aabbBuffer placed in device-local memory (#73)"); const VkDeviceAddress procAddrBefore = proc.blasAddr; const VkAccelerationStructureKHR procHandleBefore = proc.accelerationStructure; @@ -324,58 +277,6 @@ int main() { Check(devProc.aabbBuffer.buffer == VK_NULL_HANDLE, "device-buffer refit still never touched the host aabbBuffer"); - // ── Force the STAGED upload path (issue #73). On this ReBAR dev host the - // direct map path is normally taken; temporarily zero the cached upload - // budget so PreferDirectDeviceWrite returns false, exercising Mesh's - // pure-DEVICE_LOCAL + transient-staging-buffer + vkCmdCopyBuffer path - // (and its deferred-deletion of the staging buffer). The validation - // layer check below is the real assertion that the staged copy and its - // barriers are spec-correct on a fresh build and an in-place refit. ─── - { - const VkDeviceSize savedBudget = Device::directWriteBudget; - Device::directWriteBudget = 0; // 0 → PreferDirectDeviceWrite is always false - - Mesh staged; - auto verts = CubeVerts(1.0f); - auto idx = CubeIndices(); - { - VkCommandBuffer cmd = BeginCmd(); - staged.Build(verts, idx, cmd, RTBuildOptions{ - .preference = RTBuildPreference::FastTrace, .allowUpdate = true }); - SubmitWait(cmd); - } - Check(staged.blasAddr != 0, "staged-upload Build produced a non-zero blasAddr (#73)"); - // Staged geometry lives in pure DEVICE_LOCAL — no HOST_VISIBLE bit. - Check((staged.vertexBuffer.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0, - "staged vertexBuffer is device-local-only, not host-visible (#73)"); - - const VkAccelerationStructureKHR stagedHandle = staged.accelerationStructure; - { - // Same topology, deformed positions → in-place UPDATE, re-staged. - auto verts2 = CubeVerts(1.5f); - VkCommandBuffer cmd = BeginCmd(); - staged.Refit(verts2, idx, cmd); - SubmitWait(cmd); - } - Check(staged.accelerationStructure == stagedHandle, - "staged-upload Refit kept the same AS handle (in-place UPDATE, #73)"); - - // Dirty-range refit on the staged path (#119): the ranged upload must - // take the stage-a-sub-slice + vkCmdCopyBuffer-at-offset path (the - // buffer is pure DEVICE_LOCAL), keep the AS handle, and stay - // validation-clean. Only the front half [0,4) is declared dirty. - { - auto verts3 = CubeVerts(2.0f); - VkCommandBuffer cmd = BeginCmd(); - staged.Refit(verts3, idx, /*dirtyVertexOffset*/ 0, /*dirtyVertexCount*/ 4, cmd); - SubmitWait(cmd); - } - Check(staged.accelerationStructure == stagedHandle, - "staged-upload dirty-range Refit kept the same AS handle (#119)"); - - Device::directWriteBudget = savedBudget; - } - Check(Device::validationErrorCount == 0, std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); diff --git a/tests/DeferredDeletion/main.cpp b/tests/DeferredDeletion/main.cpp deleted file mode 100644 index 3d91374..0000000 --- a/tests/DeferredDeletion/main.cpp +++ /dev/null @@ -1,155 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #101: fence-keyed deferred resource-deletion queue. Since #40 dropped -// the per-frame vkQueueWaitIdle, a buffer the CPU is done with may still be -// read by the GPU for up to framesInFlight-1 more frames — so destroying it -// immediately (VulkanBuffer::Clear / the old Resize path) is a use-after-free. -// VulkanBuffer::DeferredClear() / Resize() now hand the handles to Device's -// deletion queue, tagged with the current frameCounter; ReclaimDeletions frees -// an entry only once framesInFlight frames have elapsed, and DrainDeletions -// (called after a wait-idle) frees everything unconditionally. -// -// The retire-timing is the load-bearing logic: free too early and it's the -// very UAF the queue exists to prevent; free too late and resources leak. This -// test drives it on a real headless Vulkan device (real VkBuffer/VkDeviceMemory -// so the frees actually execute and the validation layer can object), manually -// stepping Device::frameCounter to pin down the exact frame an entry is -// reclaimed on. It needs no swapchain/window — the queue itself is window-free. - -#include "vulkan/vulkan.h" -#include - -import Crafter.Graphics; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -constexpr VkBufferUsageFlags2 USAGE = - VK_BUFFER_USAGE_2_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT; -constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; - -} // namespace - -int main() { - Device::Initialize(); - - // No Window here, so set the in-flight depth ourselves (Window::Render - // normally does this at init). Use the real window depth so the timing - // matches production. - Device::framesInFlight = Window::numFrames; - Device::frameCounter = 0; - Device::deletionQueue.clear(); - - // ── DeferredClear enqueues, nulls the handle, and retires after exactly - // framesInFlight frames ────────────────────────────────────────────── - { - VulkanBuffer buf; - buf.Create(USAGE, DEVICE_LOCAL, 16); - Check(buf.buffer != VK_NULL_HANDLE, "Create produced a real buffer handle"); - - buf.DeferredClear(); - Check(buf.buffer == VK_NULL_HANDLE, - "DeferredClear nulls the handle so the buffer no longer owns it"); - Check(Device::deletionQueue.size() == 1, - "DeferredClear enqueues exactly one pending deletion"); - - // Walk right up to the retire frame: still queued at every frame before - // frameCounter reaches retireAfter + framesInFlight. - for (std::uint64_t f = 0; f < Window::numFrames; ++f) { - Device::frameCounter = f; - Device::ReclaimDeletions(); - Check(Device::deletionQueue.size() == 1, - std::format("entry survives ReclaimDeletions at frame {} " - "(< retire frame {})", f, Window::numFrames)); - } - - // At retireAfter(0) + framesInFlight it is finally freed. - Device::frameCounter = Window::numFrames; - Device::ReclaimDeletions(); - Check(Device::deletionQueue.empty(), - std::format("entry is reclaimed once frameCounter reaches {}", - Window::numFrames)); - } - - // ── Resize's reallocate path defers the OLD allocation, not the new one ── - { - Device::frameCounter = 100; - Device::deletionQueue.clear(); - - VulkanBuffer buf; - buf.Create(USAGE, DEVICE_LOCAL, 4); - VkBuffer original = buf.buffer; - - // Grow past capacity → forces destroy+recreate, which must DEFER the old - // buffer (the #63 UAF this whole queue exists to close). - buf.Resize(USAGE, DEVICE_LOCAL, 64); - Check(buf.buffer != VK_NULL_HANDLE && buf.buffer != original, - "Resize past capacity allocates a fresh buffer"); - Check(Device::deletionQueue.size() == 1 - && Device::deletionQueue.front().buffer == original, - "Resize defers the OLD allocation to the deletion queue"); - Check(Device::deletionQueue.front().retireAfter == 100, - "deferred entry is tagged with the frameCounter at enqueue time"); - - // Not yet due (tagged at 100, retires at 100 + framesInFlight). - Device::frameCounter = 100 + Window::numFrames - 1; - Device::ReclaimDeletions(); - Check(Device::deletionQueue.size() == 1, - "old allocation is not freed before its retire frame"); - - Device::frameCounter = 100 + Window::numFrames; - Device::ReclaimDeletions(); - Check(Device::deletionQueue.empty(), - "old allocation is freed on its retire frame"); - - buf.Clear(); // immediate cleanup of the live buffer (no GPU work pending) - } - - // ── DrainDeletions frees everything regardless of retire frame ─────────── - { - Device::frameCounter = 0; - Device::deletionQueue.clear(); - - VulkanBuffer a; - VulkanBuffer b; - a.Create(USAGE, DEVICE_LOCAL, 8); - b.Create(USAGE, DEVICE_LOCAL, 8); - a.DeferredClear(); - b.DeferredClear(); - Check(Device::deletionQueue.size() == 2, "two entries queued"); - - // Far from any retire frame — ReclaimDeletions would free nothing here. - Device::DrainDeletions(); - Check(Device::deletionQueue.empty(), - "DrainDeletions frees every queued entry unconditionally"); - } - - // ── EnqueueDeletion ignores a null handle (nothing to free) ────────────── - { - Device::deletionQueue.clear(); - Device::EnqueueDeletion(VK_NULL_HANDLE, VK_NULL_HANDLE); - Check(Device::deletionQueue.empty(), - "EnqueueDeletion drops a VK_NULL_HANDLE buffer"); - } - - Check(Device::validationErrorCount == 0, - std::format("no Vulkan validation errors across the run ({} seen)", - Device::validationErrorCount)); - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/FontAdvanceCache/font.ttf b/tests/FontAdvanceCache/font.ttf deleted file mode 100644 index f27f4ff..0000000 Binary files a/tests/FontAdvanceCache/font.ttf and /dev/null differ diff --git a/tests/FontAdvanceCache/main.cpp b/tests/FontAdvanceCache/main.cpp deleted file mode 100644 index e6282a5..0000000 --- a/tests/FontAdvanceCache/main.cpp +++ /dev/null @@ -1,128 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #57: Font::GetLineWidth used to call stbtt_GetCodepointHMetrics for -// every glyph on every call. Caret hit-testing (InputField_HitTestCursor) -// rescans the whole field once per character, so that is O(n²) HMetrics calls. -// Advances are now memoised per Font in *font units* (Font::AdvanceUnits): -// ASCII in a flat array, the rest in a map, rescaled per call. -// -// The cache must be in unscaled units, NOT pixels — the FontAtlas's -// Glyph::advance is baked at kBaseSize and is wrong at any other size, so it -// cannot be reused here. What is asserted: -// -// - AdvanceUnits is size-independent: the value for a codepoint is identical -// no matter which size was queried first (proves it stores units, not px). -// - GetLineWidth equals the documented pipeline: sum of per-glyph -// (int)(units * scaleForSize), with the truncation applied PER GLYPH. -// - Repeated calls (cache hits) are byte-identical to the first (miss). -// - Width scales with size — a 2× size is wider, ruling out a baked-at-one- -// size advance that would make every size render the same width. -// - The empty string is zero. -// - Codepoints above ASCII (the map path) behave like the array path. -// -// Pure CPU: Font only touches stb_truetype, so no Vulkan device is needed. - -#include - -import Crafter.Graphics; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -std::filesystem::path FindFont() { - for (const char* cand : { - "font.ttf", - "tests/FontAdvanceCache/font.ttf", - "../../examples/HelloUI/font.ttf" }) { - if (std::filesystem::exists(cand)) return cand; - } - return "font.ttf"; -} - -// The exact contract GetLineWidth must honour: truncate each glyph's scaled -// advance to int independently, then accumulate. Rescaling the cached unit -// advance per call is what keeps it correct at sizes other than kBaseSize. -std::uint32_t ExpectedWidth(Font& font, std::string_view text, float size) { - float scale = font.ScaleForSize(size); - std::uint32_t w = 0; - std::size_t i = 0; - while (i < text.size()) { - std::uint32_t cp = DecodeUtf8(text, i); - if (cp == 0) break; - w += static_cast(font.AdvanceUnits(cp) * scale); - } - return w; -} - -} // namespace - -int main() { - Font font(FindFont()); - - const std::string_view text = "Hello, World!"; - - // ── 1. AdvanceUnits is size-independent (stored in font units). ───────── - // Prime the cache via a large size, capture the unit advance, then prime - // again via a tiny size. The unit value must not move — if the cache stored - // pixels it would differ wildly between the two priming sizes. - font.GetLineWidth(text, 96.0f); - std::int32_t aBig = font.AdvanceUnits(static_cast('A')); - font.GetLineWidth(text, 6.0f); - std::int32_t aSmall = font.AdvanceUnits(static_cast('A')); - Check(aBig == aSmall && aBig > 0, - "AdvanceUnits('A') is a stable, positive font-unit value across sizes"); - - // ── 2. GetLineWidth matches the per-glyph-truncated, rescaled pipeline. ─ - bool pipelineOk = true; - for (float size : {6.0f, 11.0f, 18.0f, 32.0f, 64.0f, 100.0f}) { - std::uint32_t got = font.GetLineWidth(text, size); - std::uint32_t exp = ExpectedWidth(font, text, size); - if (got != exp) { - pipelineOk = false; - std::println(" size {}: got {} expected {}", size, got, exp); - } - } - Check(pipelineOk, "GetLineWidth == sum of per-glyph (int)(units * scale) at every size"); - - // ── 3. Cache hits are byte-identical to the first (miss) call. ────────── - std::uint32_t first = font.GetLineWidth(text, 18.0f); - bool stable = true; - for (int k = 0; k < 8; ++k) stable = stable && (font.GetLineWidth(text, 18.0f) == first); - Check(stable && first > 0, "repeated GetLineWidth calls are identical (cache hit == miss)"); - - // ── 4. Width scales with size — not baked at a single size. ───────────── - std::uint32_t w18 = font.GetLineWidth(text, 18.0f); - std::uint32_t w36 = font.GetLineWidth(text, 36.0f); - Check(w36 > w18 && w36 > w18 + (w18 / 2), - "doubling the size roughly doubles the width (advance is rescaled, not baked)"); - - // ── 5. Empty string is zero. ──────────────────────────────────────────── - Check(font.GetLineWidth("", 18.0f) == 0, "empty string has zero width"); - - // ── 6. Non-ASCII codepoints (the map path) behave like the array path. ── - // U+00E9 'é' (UTF-8 0xC3 0xA9) is > 127 so it lands in advanceUnits_, not - // the flat ASCII array. Its advance must be stable and a prefix narrower - // than the whole. - const std::string_view accented = "café"; // 'é' is two UTF-8 bytes - std::uint32_t accWidth = font.GetLineWidth(accented, 24.0f); - std::uint32_t accExp = ExpectedWidth(font, accented, 24.0f); - Check(accWidth == accExp && accWidth > font.GetLineWidth("caf", 24.0f), - "non-ASCII codepoint goes through the map path and widens the line"); - std::uint32_t eFirst = static_cast(font.AdvanceUnits(0x00E9)); - Check(static_cast(font.AdvanceUnits(0x00E9)) == eFirst, - "AdvanceUnits for a mapped codepoint is stable across calls"); - - if (failures == 0) std::println("\nAll font advance-cache checks passed."); - else std::println("\n{} font advance-cache check(s) FAILED.", failures); - return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; -} diff --git a/tests/FontAtlasDirtyRect/main.cpp b/tests/FontAtlasDirtyRect/main.cpp index 67dbda3..c136bbe 100644 --- a/tests/FontAtlasDirtyRect/main.cpp +++ b/tests/FontAtlasDirtyRect/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Regression test for issue #51: FontAtlas::Update used to re-upload the // whole 1024×1024 atlas every time a single glyph was rasterized. It now diff --git a/tests/FrameLoopSync/main.cpp b/tests/FrameLoopSync/main.cpp index ca9be69..6a1a78c 100644 --- a/tests/FrameLoopSync/main.cpp +++ b/tests/FrameLoopSync/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Issue #40: multi-frame-in-flight frame pacing. The renderer used to be // effectively single-buffered — Render() ended with an unconditional diff --git a/tests/ImageStagingRelease/main.cpp b/tests/ImageStagingRelease/main.cpp deleted file mode 100644 index 7ce4501..0000000 --- a/tests/ImageStagingRelease/main.cpp +++ /dev/null @@ -1,222 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #114: a static ImageVulkan used to keep its host-visible staging -// `buffer` (sized w*h, persistently mapped) alive for the whole life of the -// image, pinning HOST_VISIBLE / small-BAR memory long after the one upload that -// reads it has retired — and Destroy() never freed it. Update now releases it -// via VulkanBuffer::DeferredClear() right after recording the buffer→image -// copy, so the fence-keyed deletion queue (#101/#102) frees it once that -// submit's frame has cleared. A `streamed` image (the FontAtlas) keeps its -// staging, since it re-fills the persistent map and re-uploads every frame. -// -// This drives the real upload path on a headless device — no swapchain/window -// needed, a buffer→image copy only touches the queue + command pool — and -// asserts: -// - After a static Update, image.buffer owns no handle (released), and -// exactly that one allocation was handed to the deletion queue tagged with -// the current frame (so it retires later, not now). -// - The upload + readback still complete with ZERO validation errors and the -// image reads back byte-equal to the staged source — proof the staging -// genuinely outlived the submit (the queue had not yet retired it), so -// releasing it is no use-after-free. -// - The deferred entry retires (and frees) only once framesInFlight frames -// elapse, and not before. -// - A `streamed` image keeps its staging across Update and UpdateRegion (the -// FontAtlas contract), and Destroy() then frees it validation-clean. - -#include "vulkan/vulkan.h" -#include - -import Crafter.Graphics; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -VkCommandBuffer BeginCmd() { - VkCommandBufferAllocateInfo allocInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .commandPool = Device::commandPool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1, - }; - VkCommandBuffer cmd = VK_NULL_HANDLE; - Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd)); - VkCommandBufferBeginInfo beginInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - }; - Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo)); - return cmd; -} - -void SubmitWait(VkCommandBuffer cmd) { - Device::CheckVkResult(vkEndCommandBuffer(cmd)); - VkSubmitInfo submitInfo { - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .commandBufferCount = 1, - .pCommandBuffers = &cmd, - }; - Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); -} - -// RGBA8 texel as a 4-byte word, matching VK_FORMAT_R8G8B8A8_UNORM. -using Texel = std::uint32_t; - -constexpr std::uint16_t kW = 4; -constexpr std::uint16_t kH = 4; -constexpr std::uint32_t kCount = static_cast(kW) * kH; - -} // namespace - -int main() { - Device::Initialize(); - Device::validationErrorCount = 0; - - // Control the deferred-deletion clock: enqueue at frame 0, retire after 2. - Device::framesInFlight = 2; - Device::frameCounter = 0; - Device::deletionQueue.clear(); - - // ── Static texture: staging is released right after the upload copy. ────── - ImageVulkan tex; - { - VkCommandBuffer cmd = BeginCmd(); - tex.Create(kW, kH, /*mipLevels*/ 1, cmd, - VK_FORMAT_R8G8B8A8_UNORM, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT - | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - - Check(tex.buffer.buffer != VK_NULL_HANDLE, - "Create allocated the host-visible staging buffer"); - Check(!tex.streamed, "a default ImageVulkan is not streamed"); - - // Fill the staging map with a known pattern, then upload. - for (std::uint32_t i = 0; i < kCount; ++i) { - tex.buffer.value[i] = 0xA0B0C0D0u + i; - } - tex.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - - // ── The fix: staging released to the queue at record time, not pinned. ─ - Check(tex.buffer.buffer == VK_NULL_HANDLE, - "static Update releases the staging buffer (no handle pinned)"); - Check(Device::deletionQueue.size() == 1, - "exactly one allocation (the staging) handed to the deletion queue"); - const bool taggedNow = !Device::deletionQueue.empty() - && Device::deletionQueue.front().retireAfter == 0 - && Device::deletionQueue.front().buffer != VK_NULL_HANDLE; - Check(taggedNow, - "deferred staging is tagged with the enqueue frame and still a live handle"); - - // Read the image back to prove the copy saw valid staging. The upload's - // recorded copy still references the now-released staging; it must - // outlive THIS submit (queue retires at frame 2, we're at 0). Move the - // image to TRANSFER_SRC and copy into a host-visible readback buffer in - // the same command buffer. - VkImageMemoryBarrier toSrc { - .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, - .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT, - .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - .newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = tex.image, - .subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }, - }; - vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, - VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, - 0, nullptr, 1, &toSrc); - - VulkanBuffer readback; - readback.Create(VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, kCount); - VkBufferImageCopy region {}; - region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; - region.imageExtent = { kW, kH, 1 }; - vkCmdCopyImageToBuffer(cmd, tex.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - readback.buffer, 1, ®ion); - - SubmitWait(cmd); - Check(Device::validationErrorCount == 0, - "static upload + readback raised no validation errors"); - - readback.FlushHost(); - bool match = true; - for (std::uint32_t i = 0; i < kCount; ++i) { - if (readback.value[i] != 0xA0B0C0D0u + i) { match = false; break; } - } - Check(match, - "image reads back byte-equal to the staged source " - "(released staging outlived the submit)"); - } - - // ── Retire timing: the staging frees once framesInFlight frames elapse, - // and not a frame before. We've already wait-idled, so freeing is safe. ─ - Device::ReclaimDeletions(); - Check(Device::deletionQueue.size() == 1, - "staging is NOT freed before framesInFlight frames have elapsed"); - Device::frameCounter = 2; // retireAfter(0) + framesInFlight(2) <= 2 - Device::ReclaimDeletions(); - Check(Device::deletionQueue.empty(), - "staging is freed once its retire frame is reached"); - - // Destroying the image after its staging was released must not double-free - // (buffer handle is already null) and must stay validation-clean. - Device::validationErrorCount = 0; - tex.Destroy(); - Check(Device::validationErrorCount == 0, - "Destroy after a released-staging upload is validation-clean"); - - // ── Streamed image: the persistent staging is kept across uploads. ──────── - Device::frameCounter = 0; - Device::deletionQueue.clear(); - Device::validationErrorCount = 0; - { - ImageVulkan atlas; - VkCommandBuffer cmd = BeginCmd(); - atlas.Create(kW, kH, /*mipLevels*/ 1, cmd, - VK_FORMAT_R8G8B8A8_UNORM, - VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, - VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - /*streamed*/ true); - Check(atlas.streamed, "streamed=true is recorded on the image"); - - for (std::uint32_t i = 0; i < kCount; ++i) atlas.buffer.value[i] = i; - atlas.Update(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - Check(atlas.buffer.buffer != VK_NULL_HANDLE, - "streamed Update keeps the persistent staging buffer"); - // UpdateRegion (the FontAtlas re-upload path) must also keep it. - atlas.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0, 0, kW, kH); - Check(atlas.buffer.buffer != VK_NULL_HANDLE, - "streamed UpdateRegion keeps the persistent staging buffer"); - Check(Device::deletionQueue.empty(), - "a streamed image enqueues nothing for deferred deletion"); - SubmitWait(cmd); - Check(Device::validationErrorCount == 0, - "streamed upload raised no validation errors"); - - // Destroy frees the still-live persistent staging (the leak #114 names). - atlas.Destroy(); - Check(atlas.buffer.buffer == VK_NULL_HANDLE, - "Destroy frees the streamed image's persistent staging buffer"); - Check(Device::validationErrorCount == 0, - "streamed Destroy (freeing live staging) is validation-clean"); - } - - std::println("{}", failures == 0 ? "ALL PASS" : "FAILURES PRESENT"); - return failures == 0 ? 0 : 1; -} diff --git a/tests/InputFieldCaretCache/font.ttf b/tests/InputFieldCaretCache/font.ttf deleted file mode 100644 index f27f4ff..0000000 Binary files a/tests/InputFieldCaretCache/font.ttf and /dev/null differ diff --git a/tests/InputFieldCaretCache/main.cpp b/tests/InputFieldCaretCache/main.cpp deleted file mode 100644 index 033ba18..0000000 --- a/tests/InputFieldCaretCache/main.cpp +++ /dev/null @@ -1,232 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #128: DrawInputField re-walked the cursor prefix -// via Font::GetLineWidth on every frame of a focused field, even though only -// the blink (caretVisible) changes frame-to-frame. The fix memoises the prefix -// WIDTH on the InputField, keyed on (prefix bytes, fontSize) — a cheap byte -// compare guards the expensive per-glyph advance accumulation. -// -// The cache must be transparent: the emitted caret quad has to sit at exactly -// the same x as the uncached `textX + GetLineWidth(prefix)` oracle, in every -// state. This test pins: -// - the caret x matches the oracle across many (value, cursorPos, fontSize) -// states, on both the first draw (cache miss) and an immediate redraw -// (cache hit) — a hit must be byte-identical to a miss, -// - the cache invalidates correctly when value, cursorPos, or fontSize change -// (no stale caret after an edit), -// - the blink (caretVisible toggling) never moves the caret, -// - moving the field on screen (rect.x) with an unchanged value still moves -// the caret by the same delta — the WIDTH is cached, not the absolute -// caretX, so a relocated field can't get a stale caret, -// - the internal cache fields hold the measured prefix/fontSize/width after a -// draw (white-box: the memo is actually populated). -// -// Pure CPU + a TrueType file — DrawText no-ops with a null atlas/renderer, so -// only the background + caret quads are emitted and no Vulkan device or window -// is needed. The font is copied next to the binary; the test also probes the -// project-root path. - -#include - -import Crafter.Graphics; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -std::filesystem::path FindFont() { - for (const char* cand : { - "font.ttf", - "tests/InputFieldCaretCache/font.ttf", - "../../examples/HelloUI/font.ttf" }) { - if (std::filesystem::exists(cand)) return cand; - } - return "font.ttf"; -} - -const InputFieldColors kColors{ - {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, - {1, 1, 1, 1}, {1, 1, 1, 1}, -}; - -constexpr float kEps = 1e-3f; - -// Draw the field into a fresh quad buffer and return the caret quad's x, or -// NaN if no caret was emitted (not focused / not visible). The caret is the -// quad emitted after the background quad. atlas/renderer left null so DrawText -// is a no-op — only the bg + caret quads land. -float CaretX(const InputField& f, Rect rect, Font& font, float fontSize, - bool caretVisible) { - std::array quads{}; - std::uint32_t count = 0; - UIBuffer buf; - buf.quads = quads.data(); - buf.quadCount = &count; - buf.quadCap = static_cast(quads.size()); - - DrawInputField(buf, f, rect, font, fontSize, kColors, caretVisible); - - // bg quad always emitted (count>=1); caret quad, if any, is the last one. - if (count < 2) return std::numeric_limits::quiet_NaN(); - return quads[count - 1].x; -} - -// Uncached oracle: where the caret should sit, computed straight from -// GetLineWidth on the prefix — the exact arithmetic DrawInputField uses, with -// no memo in the path. -float OracleCaretX(const InputField& f, Rect rect, Font& font, float fontSize) { - float textX = rect.x + kColors.paddingX; - std::string_view sub(f.value.data(), std::min(f.cursorPos, f.value.size())); - float w = sub.empty() ? 0.0f - : static_cast(font.GetLineWidth(sub, fontSize)); - return textX + w; -} - -bool Near(float a, float b) { return std::abs(a - b) <= kEps; } - -} // namespace - -int main() { - Font font(FindFont()); - - const float rectX = 30.0f; - const std::array samples = { - "Hello, world", - "1234567890", - "iiiiWWWWmmmm", - "caf\xC3\xA9 na\xC3\xAFve \xE2\x9C\x93", // "café naïve ✓" — multibyte - }; - const std::array sizes = {12.0f, 18.0f, 27.5f}; - - // 1. Across every (value, cursorPos, fontSize), the cached caret matches the - // uncached oracle — and a redraw (guaranteed cache hit) matches the first - // draw exactly. - bool sweepOk = true, hitMatchesMiss = true; - for (std::string_view s : samples) { - for (float size : sizes) { - // Walk every codepoint boundary as a cursor position. - for (std::size_t pos = 0; pos <= s.size(); ++pos) { - InputField f; - f.value = std::string(s); - f.focused = true; - f.cursorPos = pos; - Rect rect{rectX, 0.0f, 200.0f, 24.0f}; - - float miss = CaretX(f, rect, font, size, true); // populates cache - float hit = CaretX(f, rect, font, size, true); // reads cache - float oracle = OracleCaretX(f, rect, font, size); - - if (!Near(miss, oracle)) { - sweepOk = false; - std::println(" caret mismatch \"{}\" pos={} size={}: got={} oracle={}", - s, pos, size, miss, oracle); - } - if (!Near(miss, hit)) hitMatchesMiss = false; - } - } - } - Check(sweepOk, "cached caret x matches the uncached GetLineWidth oracle across states"); - Check(hitMatchesMiss, "a cache hit (redraw) is byte-identical to the cache miss"); - - // 2. The cache invalidates on each keyed input. Reuse ONE field across the - // mutations so a stale memo would survive into the next draw if the key - // check were wrong. - { - InputField f; - f.value = "abcdef"; - f.focused = true; - Rect rect{rectX, 0.0f, 200.0f, 24.0f}; - constexpr float size = 18.0f; - - f.cursorPos = 3; - (void) CaretX(f, rect, font, size, true); // prime cache at (abcdef,3,18) - - // 2a. cursorPos change. - f.cursorPos = 6; - Check(Near(CaretX(f, rect, font, size, true), OracleCaretX(f, rect, font, size)), - "caret updates when cursorPos changes (cache key includes cursorPos)"); - - // 2b. value change with the SAME cursorPos and a prefix of the same - // length — only the bytes differ, so a length-only key would miss - // this. Widen a glyph to force a different width. - f.value = "abcWWW"; - f.cursorPos = 6; - Check(Near(CaretX(f, rect, font, size, true), OracleCaretX(f, rect, font, size)), - "caret updates when value bytes change (cache key compares bytes, not length)"); - - // 2c. fontSize change with value/cursor fixed. - float small = CaretX(f, rect, font, 12.0f, true); - float big = CaretX(f, rect, font, 27.5f, true); - Check(Near(small, OracleCaretX(f, rect, font, 12.0f)) && - Near(big, OracleCaretX(f, rect, font, 27.5f)) && - !Near(small, big), - "caret updates when fontSize changes (cache key includes fontSize)"); - } - - // 3. The blink must never move the caret: drawing with caretVisible=true, - // then false (no caret), then true again yields the same x. - { - InputField f; - f.value = "blink test"; - f.focused = true; - f.cursorPos = 5; - Rect rect{rectX, 0.0f, 200.0f, 24.0f}; - constexpr float size = 18.0f; - - float on1 = CaretX(f, rect, font, size, true); - float off = CaretX(f, rect, font, size, false); // caret suppressed - float on2 = CaretX(f, rect, font, size, true); - Check(std::isnan(off) && Near(on1, on2) && Near(on1, OracleCaretX(f, rect, font, size)), - "blink (caretVisible toggle) does not move or stale the caret"); - } - - // 4. Moving the field on screen with an UNCHANGED value moves the caret by - // exactly the rect delta. This is the guard for caching the prefix WIDTH - // rather than the absolute caretX — a memoised caretX would go stale here. - { - InputField f; - f.value = "relocate"; - f.focused = true; - f.cursorPos = 8; - constexpr float size = 18.0f; - - Rect a{rectX, 0.0f, 200.0f, 24.0f}; - Rect b{rectX + 137.0f, 0.0f, 200.0f, 24.0f}; - float xa = CaretX(f, a, font, size, true); // primes cache - float xb = CaretX(f, b, font, size, true); // same value → cache hit on width - Check(Near(xb - xa, 137.0f) && Near(xb, OracleCaretX(f, b, font, size)), - "moving the field shifts the caret by the rect delta (width cached, not caretX)"); - } - - // 5. White-box: after a draw the memo holds the measured prefix, fontSize - // and width — confirming the fast path is actually wired, not dead code. - { - InputField f; - f.value = "memo check"; - f.focused = true; - f.cursorPos = 4; - Rect rect{rectX, 0.0f, 200.0f, 24.0f}; - constexpr float size = 18.0f; - (void) CaretX(f, rect, font, size, true); - - std::string_view sub(f.value.data(), f.cursorPos); - float w = static_cast(font.GetLineWidth(sub, size)); - Check(f.caretCachePrefix_ == sub && - f.caretCacheFontSize_ == size && - Near(f.caretCacheWidth_, w), - "draw populates the caret memo with the measured prefix/fontSize/width"); - } - - if (failures == 0) std::println("\nAll InputField caret-cache checks passed."); - else std::println("\n{} InputField caret-cache check(s) FAILED.", failures); - return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; -} diff --git a/tests/InputFieldHitTest/main.cpp b/tests/InputFieldHitTest/main.cpp index a110748..0961337 100644 --- a/tests/InputFieldHitTest/main.cpp +++ b/tests/InputFieldHitTest/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Regression test for issue #56: InputField_HitTestCursor used to map a click // x-coord to a cursor byte offset by calling Font::GetLineWidth on every diff --git a/tests/MemoryTypeFallback/main.cpp b/tests/MemoryTypeFallback/main.cpp index 33df327..a0c0ee9 100644 --- a/tests/MemoryTypeFallback/main.cpp +++ b/tests/MemoryTypeFallback/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Regression test for issue #59: Device::GetMemoryType used to be a bare // first-superset match that threw whenever no memory type satisfied the diff --git a/tests/MeshDecompressStagingRelease/main.cpp b/tests/MeshDecompressStagingRelease/main.cpp deleted file mode 100644 index d18fcc7..0000000 --- a/tests/MeshDecompressStagingRelease/main.cpp +++ /dev/null @@ -1,244 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #67: the compressed Mesh::Build path used to keep `compressedStaging` -// — a per-mesh host-visible staging buffer holding the GDeflate streams — -// alive for the whole life of the mesh, pinning host-visible memory long after -// the one decompress that reads it has retired. Build now releases it via -// VulkanBuffer::DeferredClear() right after recording the decompress, so the -// fence-keyed deletion queue (#101/#102) frees it once that submit's frame has -// cleared instead of pinning it forever. -// -// This drives the real hardware GPU-decompress path (VK_EXT_memory_decompression, -// GDeflate 1.0) on a headless device — no swapchain/window needed, a decompress + -// BLAS build only touches the queue + command pool — and asserts: -// - After Build, mesh.compressedStaging owns no handle (released). -// - Exactly that one allocation was handed to Device's deletion queue, tagged -// with the current frameCounter (so it retires on a later frame, not now). -// - The decompress + BLAS build still complete with ZERO validation errors and -// a non-zero BLAS address — proof the staging genuinely outlived the submit -// (the queue had not yet retired it), so this is no use-after-free. -// - The GPU-decompressed vertex/index data read back byte-equal to the source, -// so releasing the staging didn't corrupt the result. -// - The deferred entry actually retires (and frees) once framesInFlight frames -// elapse, and not before — the whole point of routing through the queue. -// -// As a regression guard for issue #110 it then repeats the Build with -// allowUpdate=false and asserts the queue holds exactly TWO entries — the -// compressed staging PLUS the dead per-mesh BLAS scratch a static build can no -// longer refit. That extra scratch was the unaccounted-for entry behind #110 -// (the original test did a static build yet asserted size==1); pinning the -// breakdown keeps the count above a known, named quantity. -// -// On hardware without VK_EXT_memory_decompression the compressed Build takes the -// CPU-decode fallback, which never allocates compressedStaging, so the new -// behavior is moot — the test reports that and skips the GPU-path assertions. - -#include "vulkan/vulkan.h" -#include - -import Crafter.Graphics; -import Crafter.Asset; -import Crafter.Math; -import std; - -using namespace Crafter; -namespace fs = std::filesystem; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// One-time command buffer helpers — record, submit, block. Mirrors the other -// headless RT tests (BLASBuildOptions). -VkCommandBuffer BeginCmd() { - VkCommandBufferAllocateInfo allocInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .commandPool = Device::commandPool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1, - }; - VkCommandBuffer cmd = VK_NULL_HANDLE; - Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd)); - VkCommandBufferBeginInfo beginInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - }; - Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo)); - return cmd; -} - -void SubmitWait(VkCommandBuffer cmd) { - Device::CheckVkResult(vkEndCommandBuffer(cmd)); - VkSubmitInfo submitInfo { - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .commandBufferCount = 1, - .pCommandBuffers = &cmd, - }; - Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); -} - -// A unit cube (8 verts, 36 indices) — small enough to fit one GDeflate tile, -// enough topology for a real BLAS. -MeshAsset MakeCubeMesh() { - MeshAsset mesh; - mesh.vertexes = { - {-1.f, -1.f, -1.f}, { 1.f, -1.f, -1.f}, - { 1.f, 1.f, -1.f}, {-1.f, 1.f, -1.f}, - {-1.f, -1.f, 1.f}, { 1.f, -1.f, 1.f}, - { 1.f, 1.f, 1.f}, {-1.f, 1.f, 1.f}, - }; - mesh.indexes = { - 0,1,2, 0,2,3, 4,6,5, 4,7,6, - 0,4,5, 0,5,1, 3,2,6, 3,6,7, - 1,5,6, 1,6,2, 0,3,7, 0,7,4, - }; - return mesh; -} - -} // namespace - -int main() { - Device::Initialize(); - Device::validationErrorCount = 0; - - // Build → compress → load a cube via the real asset pipeline. - MeshAsset srcMesh = MakeCubeMesh(); - const fs::path meshPath = - fs::temp_directory_path() / "crafter_meshdecompress_staging_release.cmesh"; - srcMesh.SaveCompressed(meshPath); - CompressedMeshAsset asset = LoadCompressedMesh(meshPath); - fs::remove(meshPath); - - Check(asset.vertexCount == srcMesh.vertexes.size() - && asset.indexCount == srcMesh.indexes.size(), - "LoadCompressedMesh round-trips the cube header"); - - if (!Device::memoryDecompressionSupported) { - // No GPU codec → compressed Build takes the CPU-decode fallback, which - // never allocates compressedStaging. The release behavior under test - // only exists on the GPU path, so there is nothing to assert here. - std::println("VK_EXT_memory_decompression absent — GPU decompress path " - "not exercised; skipping staging-release assertions."); - return failures == 0 ? 0 : 1; - } - - // Control the deferred-deletion clock: enqueue at frame 0, retire after 2. - Device::framesInFlight = 2; - Device::frameCounter = 0; - Device::deletionQueue.clear(); - - Mesh mesh; - VkCommandBuffer cmd = BeginCmd(); - // allowUpdate=true so the per-mesh BLAS scratch is RETAINED for refit - // (issue #66) rather than deferred-cleared after the build. A static - // (allowUpdate=false) build would also hand its scratch to the deletion - // queue, making the count below 2; keeping it isolates this test to the - // one allocation it cares about — the released compressed staging (#67). - // The staging-release behavior under test is independent of allowUpdate. - mesh.Build(asset, cmd, RTBuildOptions{ .allowUpdate = true }); - - // ── The fix: staging is released to the queue at record time, not pinned. ── - Check(mesh.compressedStaging.buffer == VK_NULL_HANDLE, - "Build releases compressedStaging (no handle pinned to the mesh)"); - Check(Device::deletionQueue.size() == 1, - "exactly one allocation (the staging) handed to the deletion queue"); - const bool taggedNow = !Device::deletionQueue.empty() - && Device::deletionQueue.front().retireAfter == 0 - && Device::deletionQueue.front().buffer != VK_NULL_HANDLE; - Check(taggedNow, - "deferred staging is tagged with the enqueue frame and still a live handle"); - - // The decompress recorded above still references the staging's address; it - // must outlive THIS submit. The queue hasn't retired it (frame 0, retires at - // 2), so the GPU reads valid memory. Zero validation errors + a real BLAS - // address prove there's no use-after-free. - SubmitWait(cmd); - Check(mesh.blasAddr != 0, "compressed Build produced a non-zero BLAS address"); - Check(Device::validationErrorCount == 0, - "decompress + BLAS build raised no validation errors"); - - // The GPU decompress wrote into the device-local vertex/index buffers - // (issue #73 placed RT geometry in VRAM, so they are no longer host-mapped). - // Copy them back to host-visible staging — the proper way to inspect - // device-local memory — and compare: releasing the compressed staging must - // not have corrupted the decompressed result. The geometry buffers carry - // TRANSFER_SRC for exactly this (#73). - VulkanBuffer, true> vertReadback; - VulkanBuffer idxReadback; - vertReadback.Resize(VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast(srcMesh.vertexes.size())); - idxReadback.Resize(VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, static_cast(srcMesh.indexes.size())); - { - // Safe to copy without a barrier: SubmitWait above wait-idled the queue, - // so the decompress + BLAS reads of these buffers have fully retired. - VkCommandBuffer rcmd = BeginCmd(); - VkBufferCopy vRegion { .srcOffset = 0, .dstOffset = 0, .size = vertReadback.size }; - vkCmdCopyBuffer(rcmd, mesh.vertexBuffer.buffer, vertReadback.buffer, 1, &vRegion); - VkBufferCopy iRegion { .srcOffset = 0, .dstOffset = 0, .size = idxReadback.size }; - vkCmdCopyBuffer(rcmd, mesh.indexBuffer.buffer, idxReadback.buffer, 1, &iRegion); - SubmitWait(rcmd); - } - vertReadback.FlushHost(); - idxReadback.FlushHost(); - const bool vertsMatch = std::memcmp( - vertReadback.value, srcMesh.vertexes.data(), - srcMesh.vertexes.size() * sizeof(srcMesh.vertexes[0])) == 0; - const bool idxMatch = std::memcmp( - idxReadback.value, srcMesh.indexes.data(), - srcMesh.indexes.size() * sizeof(srcMesh.indexes[0])) == 0; - Check(vertsMatch, "GPU-decompressed vertices are byte-equal to the source"); - Check(idxMatch, "GPU-decompressed indices are byte-equal to the source"); - - // ── Retire timing: the staging frees once framesInFlight frames elapse, - // and not a frame before. We've already wait-idled, so freeing is safe. ── - Device::ReclaimDeletions(); - Check(Device::deletionQueue.size() == 1, - "staging is NOT freed before framesInFlight frames have elapsed"); - Device::frameCounter = 2; // retireAfter(0) + framesInFlight(2) <= 2 - Device::ReclaimDeletions(); - Check(Device::deletionQueue.empty(), - "staging is freed once its retire frame is reached"); - - // ── Regression guard for issue #110 ────────────────────────────────────── - // The "exactly one allocation" assertion above holds ONLY because the - // refit-capable build (allowUpdate=true) RETAINS its per-mesh BLAS scratch - // for in-place updates (#66). A static (allowUpdate=false) build cannot - // refit, so it hands its now-dead scratch to the deletion queue too — the - // SAME Build then lands TWO deferred allocations: the compressed staging - // (#67) AND the dead scratch. Issue #110 was the original test doing a - // static build and asserting size==1: the scratch was the unaccounted-for - // second entry. Pin that breakdown down explicitly so a future change to - // scratch deferral can't silently shift the count the #67 assertion relies - // on, and so the "extra deferred allocation" stays a known, named quantity. - Device::frameCounter = 0; - Device::deletionQueue.clear(); - Mesh staticMesh; - VkCommandBuffer scmd = BeginCmd(); - staticMesh.Build(asset, scmd, RTBuildOptions{ .allowUpdate = false }); - Check(staticMesh.compressedStaging.buffer == VK_NULL_HANDLE, - "static Build also releases compressedStaging (#67 is allowUpdate-independent)"); - Check(Device::deletionQueue.size() == 2, - "static Build enqueues staging + dead BLAS scratch (the #110 extra deletion)"); - SubmitWait(scmd); - Check(staticMesh.blasAddr != 0, "static compressed Build produced a non-zero BLAS address"); - Check(Device::validationErrorCount == 0, - "static decompress + BLAS build raised no validation errors"); - // Retire both entries (enqueued at frame 0, retire at framesInFlight) so the - // staticMesh's scratch/staging are freed and nothing leaks past this test. - Device::frameCounter = Device::framesInFlight; - Device::ReclaimDeletions(); - Check(Device::deletionQueue.empty(), - "static Build's staging + scratch both retire once their frame elapses"); - - std::println("{}", failures == 0 ? "ALL PASS" : "FAILURES PRESENT"); - return failures == 0 ? 0 : 1; -} diff --git a/tests/MipChainBarrierBatch/main.cpp b/tests/MipChainBarrierBatch/main.cpp deleted file mode 100644 index f448135..0000000 --- a/tests/MipChainBarrierBatch/main.cpp +++ /dev/null @@ -1,106 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #70: ImageVulkan's mip-chain upload used to issue -// N+1 layout-transition barriers per chain, one vkCmdPipelineBarrier per call. -// The final blit's destination level is never read again, so its dedicated -// TRANSFER_DST -> TRANSFER_SRC barrier is redundant; the chain now folds that -// last level straight into the final transition, which batches two -// VkImageMemoryBarrier entries into a single vkCmdPipelineBarrier: -// - levels [0, mipLevels-1): TRANSFER_SRC_OPTIMAL -> consumer layout -// - level mipLevels-1 : TRANSFER_DST_OPTIMAL -> consumer layout -// BuildMipChainFinalBarriers is the pure builder for that batched barrier, so -// this test drives it directly with a sentinel image handle — no GPU device -// needed at runtime, mirroring MemoryTypeFallback / UploadStrategy. - -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// A non-null sentinel so we can assert the builder propagates the image handle -// into every barrier without owning a real VkImage. -VkImage SentinelImage() { - return reinterpret_cast(static_cast(0xC0FFEE)); -} - -// Common invariants every emitted barrier must satisfy regardless of which -// level group it covers. -void CheckCommon(const VkImageMemoryBarrier& b, VkImageLayout layout, std::string_view tag) { - Check(b.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, std::format("{}: sType", tag)); - Check(b.image == SentinelImage(), std::format("{}: image handle propagated", tag)); - Check(b.newLayout == layout, std::format("{}: newLayout is the consumer layout", tag)); - Check(b.dstAccessMask == VK_ACCESS_SHADER_READ_BIT, std::format("{}: dst access is shader read", tag)); - Check(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, std::format("{}: src queue ignored", tag)); - Check(b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, std::format("{}: dst queue ignored", tag)); - Check(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT, std::format("{}: color aspect", tag)); - Check(b.subresourceRange.baseArrayLayer == 0, std::format("{}: base array layer 0", tag)); - Check(b.subresourceRange.layerCount == 1, std::format("{}: single array layer", tag)); -} - -} // namespace - -int main() { - constexpr VkImageLayout LAYOUT = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - - for (std::uint32_t mipLevels : {2u, 3u, 5u, 12u}) { - std::array barriers; - std::uint32_t count = BuildMipChainFinalBarriers(SentinelImage(), LAYOUT, mipLevels, barriers); - - // The whole point: the final transition is a single batched call of two - // barriers, never one-per-level. - Check(count == 2, std::format("mip={}: batched into exactly two barriers", mipLevels)); - - const VkImageMemoryBarrier& src = barriers[0]; - const VkImageMemoryBarrier& dst = barriers[1]; - CheckCommon(src, LAYOUT, std::format("mip={} src-group", mipLevels)); - CheckCommon(dst, LAYOUT, std::format("mip={} dst-group", mipLevels)); - - // Group 0: the blit sources, [0, mipLevels-1), coming from TRANSFER_SRC. - Check(src.oldLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - std::format("mip={}: source group transitions from TRANSFER_SRC", mipLevels)); - Check(src.srcAccessMask == VK_ACCESS_TRANSFER_READ_BIT, - std::format("mip={}: source group src access is transfer read", mipLevels)); - Check(src.subresourceRange.baseMipLevel == 0, - std::format("mip={}: source group starts at level 0", mipLevels)); - Check(src.subresourceRange.levelCount == mipLevels - 1, - std::format("mip={}: source group covers all but the last level", mipLevels)); - - // Group 1: the final blit's destination, still TRANSFER_DST, never read. - Check(dst.oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - std::format("mip={}: last level transitions from TRANSFER_DST", mipLevels)); - Check(dst.srcAccessMask == VK_ACCESS_TRANSFER_WRITE_BIT, - std::format("mip={}: last level src access is transfer write", mipLevels)); - Check(dst.subresourceRange.baseMipLevel == mipLevels - 1, - std::format("mip={}: last level starts at the final mip", mipLevels)); - Check(dst.subresourceRange.levelCount == 1, - std::format("mip={}: last level covers a single mip", mipLevels)); - - // The two groups must tile [0, mipLevels) with no gap and no overlap, or - // some level would be left in a transfer layout when the shader samples. - std::uint32_t covered = src.subresourceRange.levelCount + dst.subresourceRange.levelCount; - Check(covered == mipLevels, - std::format("mip={}: the two groups cover every level exactly once", mipLevels)); - Check(src.subresourceRange.baseMipLevel + src.subresourceRange.levelCount - == dst.subresourceRange.baseMipLevel, - std::format("mip={}: groups are contiguous (no gap/overlap)", mipLevels)); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/MouseScroll/main.cpp b/tests/MouseScroll/main.cpp index b7f2d9c..04be40e 100644 --- a/tests/MouseScroll/main.cpp +++ b/tests/MouseScroll/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Regression test for issue #32: the Wayland wl_pointer.axis handler used to // be an empty stub, so Window::onMouseScroll (and with it MouseScrollBind) diff --git a/tests/PipelineCacheValidation/main.cpp b/tests/PipelineCacheValidation/main.cpp deleted file mode 100644 index abec896..0000000 --- a/tests/PipelineCacheValidation/main.cpp +++ /dev/null @@ -1,141 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #69: the engine feeds a shared Device::pipelineCache -// to every vkCreate*Pipelines call and persists it across runs. A blob written -// by a different GPU (or a corrupt/short file) must be rejected before it is -// handed to vkCreatePipelineCache, otherwise the driver ignores or rejects it. -// Device::PipelineCacheDataCompatible is that gate: pure logic over the standard -// 32-byte VkPipelineCacheHeaderVersionOne header (headerSize, headerVersion, -// vendorID, deviceID, pipelineCacheUUID) compared against Device::deviceProperties. -// It needs no GPU, so this test stamps synthetic device identities and headers -// and drives it directly, mirroring MemoryTypeFallback / UploadStrategy. - -#include -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// Stamp the device identity the validator compares against. -void SetDevice(std::uint32_t vendorID, std::uint32_t deviceID, - const std::array& uuid) { - Device::deviceProperties = {}; - Device::deviceProperties.vendorID = vendorID; - Device::deviceProperties.deviceID = deviceID; - std::memcpy(Device::deviceProperties.pipelineCacheUUID, uuid.data(), VK_UUID_SIZE); -} - -// Build a 32-byte VkPipelineCacheHeaderVersionOne blob, optionally with extra -// trailing payload bytes (the real cache body). Fields are written little-endian -// at fixed offsets, matching how the driver lays the header out on disk. -std::vector MakeHeader(std::uint32_t headerSize, - std::uint32_t headerVersion, - std::uint32_t vendorID, - std::uint32_t deviceID, - const std::array& uuid, - std::size_t trailing = 0) { - std::vector blob(16 + VK_UUID_SIZE + trailing, std::byte{0xAB}); - auto put = [&](std::size_t off, std::uint32_t v) { - std::memcpy(blob.data() + off, &v, sizeof(v)); - }; - put(0, headerSize); - put(4, headerVersion); - put(8, vendorID); - put(12, deviceID); - std::memcpy(blob.data() + 16, uuid.data(), VK_UUID_SIZE); - return blob; -} - -constexpr std::uint32_t kVendor = 0x10DE; // NVIDIA -constexpr std::uint32_t kDevice = 0x2204; // some GPU device id -constexpr std::array kUuid = { - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 -}; -constexpr std::uint32_t kV1 = VK_PIPELINE_CACHE_HEADER_VERSION_ONE; -constexpr std::uint32_t kHeaderSize = 16 + VK_UUID_SIZE; // 32 - -} // namespace - -int main() { - SetDevice(kVendor, kDevice, kUuid); - - // --- the happy path: a header written by this exact device -------------- - { - auto blob = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid); - Check(Device::PipelineCacheDataCompatible(blob), - "matching vendor/device/UUID header is accepted"); - - auto withBody = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid, /*trailing*/ 4096); - Check(Device::PipelineCacheDataCompatible(withBody), - "matching header followed by a cache body is accepted"); - } - - // --- foreign / stale blobs must be rejected ----------------------------- - { - auto otherVendor = MakeHeader(kHeaderSize, kV1, 0x1002 /*AMD*/, kDevice, kUuid); - Check(!Device::PipelineCacheDataCompatible(otherVendor), - "different vendorID is rejected"); - - auto otherDevice = MakeHeader(kHeaderSize, kV1, kVendor, 0x9999, kUuid); - Check(!Device::PipelineCacheDataCompatible(otherDevice), - "different deviceID is rejected"); - - std::array otherUuid = kUuid; - otherUuid[15] ^= 0xFF; // a driver update bumps the UUID - auto staleUuid = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, otherUuid); - Check(!Device::PipelineCacheDataCompatible(staleUuid), - "different pipelineCacheUUID (e.g. driver update) is rejected"); - } - - // --- malformed headers -------------------------------------------------- - { - auto badVersion = MakeHeader(kHeaderSize, 0xDEAD, kVendor, kDevice, kUuid); - Check(!Device::PipelineCacheDataCompatible(badVersion), - "unknown headerVersion is rejected"); - - auto smallHeaderSize = MakeHeader(8, kV1, kVendor, kDevice, kUuid); - Check(!Device::PipelineCacheDataCompatible(smallHeaderSize), - "headerSize smaller than the 32-byte header is rejected"); - - Check(!Device::PipelineCacheDataCompatible({}), - "empty blob (no file / cold start) is rejected"); - - std::vector truncated(20, std::byte{0}); - Check(!Device::PipelineCacheDataCompatible(truncated), - "blob too short to hold the header is rejected"); - } - - // --- identity follows the active device --------------------------------- - { - // Re-stamp as a different device; a blob valid for the old one is now - // foreign. Guards against the validator caching identity anywhere but - // Device::deviceProperties. - auto blob = MakeHeader(kHeaderSize, kV1, kVendor, kDevice, kUuid); - SetDevice(0x8086 /*Intel*/, 0x1234, kUuid); - Check(!Device::PipelineCacheDataCompatible(blob), - "a blob from the previous device is rejected after the device changes"); - SetDevice(kVendor, kDevice, kUuid); - Check(Device::PipelineCacheDataCompatible(blob), - "...and accepted again once the matching device is restored"); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/PushConstantRewrite/main.cpp b/tests/PushConstantRewrite/main.cpp index 486cec1..78bee47 100644 --- a/tests/PushConstantRewrite/main.cpp +++ b/tests/PushConstantRewrite/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Regression test for issue #18: the NVIDIA descriptor-heap AS-read workaround // (WorkaroundNvidiaAS::Patch) used to bolt a brand-new push-constant block onto diff --git a/tests/SetupCmdBufferReuse/main.cpp b/tests/SetupCmdBufferReuse/main.cpp deleted file mode 100644 index ae25736..0000000 --- a/tests/SetupCmdBufferReuse/main.cpp +++ /dev/null @@ -1,115 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #153: two distinct validation errors the frame loop emitted, both -// reproduced here by driving the *real* loop against a live compositor and -// asserting the Vulkan validation layer stays silent — same harness idea as -// FrameLoopSync (the silent layer is the load-bearing check). -// -// Problem 1 — per-frame acquire-barrier access/stage mismatch. The -// acquire->GENERAL barrier hardcoded dstAccessMask = -// SHADER_WRITE | TRANSFER_WRITE but used the per-pass stage union as its dst -// stage mask. For an all-compute frame the union narrows to COMPUTE_SHADER, -// which does NOT support TRANSFER_WRITE, so VUID-02820 fired every frame. -// FrameLoopSync runs with NO passes, where the union falls back to the -// conservative writer union (which DOES include TRANSFER) and the VUID never -// fires — so it cannot catch this. The key here is a real compute pass that -// narrows SwapchainStage() to COMPUTE_SHADER, exactly like UIRenderer. -// -// Problem 2 — mid-session StartInit/FinishInit reuse the shared -// drawCmdBuffers[currentBuffer]. With no steady-state wait-idle the loop's -// last submission of that buffer is still in flight when setup runs, so the -// old code re-began it (VUID-vkBeginCommandBuffer-commandBuffer-00049) and -// re-submitted it (VUID-vkQueueSubmit-pCommandBuffers-00071) while pending, -// and any resource freed in the StartInit..FinishInit bracket could still be -// referenced by that submission. StartInit/GetCmd now drain the queue first. -// -// Needs a live Wayland compositor + a Vulkan device at runtime (same as the -// windowed examples / FrameLoopSync), so it shares the native build settings -// and is Linux-only. - -#include "vulkan/vulkan.h" -#include - -import Crafter.Graphics; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// A minimal compute pass: records nothing, but narrows its swapchain write -// stage to COMPUTE_SHADER like every real menu/game pass. That narrowing is -// what shrinks the frame's stage union to COMPUTE_SHADER and so exposes the -// problem-1 acquire-barrier access/stage mismatch. Record() needs no work — -// the bug is in the frame-edge barriers the loop records around the passes, -// not in the pass body. -struct ComputePass : RenderPass { - void Record(GraphicsCommandBuffer, std::uint32_t, Window&) override {} - VkPipelineStageFlags SwapchainStage() const override { - return VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT; - } -}; - -} // namespace - -int main() { - Device::Initialize(); - - Window window(640, 480, "SetupCmdBufferReuse test"); - - ComputePass pass; - window.passes.push_back(&pass); - - // ── Problem 1: all-compute frames ─────────────────────────────────────── - // Run enough frames that the acquire barrier is recorded many times with - // swapWriterStages == COMPUTE_SHADER. Pre-fix this logged VUID-02820 once - // per frame. - constexpr int kWarmupFrames = 8; - for (int i = 0; i < kWarmupFrames; ++i) window.Render(); - - Check(Device::validationErrorCount == 0, - std::format("no validation errors over {} all-compute frames " - "(acquire-barrier access/stage match) ({} seen)", - kWarmupFrames, Device::validationErrorCount)); - - // ── Problem 2: mid-session setup while a frame is in flight ────────────── - // After Render() returns there is no steady-state wait-idle, so the last - // submission of drawCmdBuffers[currentBuffer] is still pending. Calling - // StartInit()/FinishInit() now (as a scene transition would, to build map - // meshes / acceleration structures) reuses that exact command buffer. The - // setup buffer is left empty on purpose: it's the begin/submit lifecycle - // around the in-flight buffer that the old code got wrong, not the - // contents. Interleave more frames so each StartInit again races a fresh - // in-flight submission. - constexpr int kSetupRounds = 3; - for (int r = 0; r < kSetupRounds; ++r) { - VkCommandBuffer cmd = window.StartInit(); - Check(cmd != VK_NULL_HANDLE, "StartInit returns a usable command buffer"); - window.FinishInit(); - for (int f = 0; f < 4; ++f) window.Render(); - } - - Check(Device::validationErrorCount == 0, - std::format("no validation errors across {} mid-session StartInit/" - "FinishInit rounds interleaved with rendering ({} seen)", - kSetupRounds, Device::validationErrorCount)); - - // Drain before teardown — no steady-state wait-idle means GPU work may - // still be in flight when the loop exits. - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/ShapeTextCache/main.cpp b/tests/ShapeTextCache/main.cpp index 545425e..3ef990b 100644 --- a/tests/ShapeTextCache/main.cpp +++ b/tests/ShapeTextCache/main.cpp @@ -1,5 +1,21 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +/* +Crafter®.Graphics +Copyright (C) 2026 Catcrafts® +catcrafts.net + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License version 3.0 as published by the Free Software Foundation; + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ // Issue #52: shaped-run cache for UIRenderer::ShapeText. The slow path // (UTF-8 decode + per-glyph atlas lookup + layout) is memoized into an @@ -209,63 +225,6 @@ int main() { bufBig.data(), bufBig.size(), &advBig); Check(advBig > advHit * 1.5f, "a larger pxSize produces a wider advance"); - // ── 8. Overflow evicts the least-recently-used run, not the whole cache. ─ - // (Issue #123) The old policy did a full clear() at the cap, periodically - // nuking the stable labels and forcing a full-UI reshape the next frame. - // The LRU policy instead pins the cache at its cap (evict one, add one) and - // keeps the hot, reshaped-every-frame set resident. A hit/miss and a - // re-shape of already-rasterised glyphs are not observable through the - // output or atlas.dirty, so this is asserted via the cache introspection - // hooks added for the issue. - ui.InvalidateFont(font); - Check(ui.ShapedRunCacheSize() == 0, "cache empty after InvalidateFont"); - - std::array sink{}; - auto shapeUnique = [&](std::size_t n) { - // Small fixed glyph set (digits + "run#"), so the atlas rasterises once - // and every call is still a distinct cache key. - std::string s = std::format("run#{}", n); - ui.ShapeText(font, kSize, 0, 0, s, white, sink.data(), sink.size(), nullptr); - }; - - // Insert distinct keys until the size stops growing on a brand-new insert. - // Under evict-one LRU that plateau IS the cap (evict one, add one → size - // unchanged). Under the old clear()-all policy a new insert at the cap - // drops the size to 1, so it would never plateau and `cap` would stay 0. - std::size_t cap = 0, prev = 0; - for (std::size_t i = 0; i < 100000 && cap == 0; ++i) { - shapeUnique(i); - std::size_t now = ui.ShapedRunCacheSize(); - if (now == prev) cap = now; - prev = now; - } - Check(cap > 0, "cache size plateaus at a fixed cap (evict-one LRU, not clear-all)"); - - // Hot, every-frame label stays resident across a churn far exceeding the - // cap, because reshaping it each iteration keeps it most-recently-used. - const std::string_view kHot = "Hot Label"; - ui.ShapeText(font, kSize, 0, 0, kHot, white, sink.data(), sink.size(), nullptr); - bool hotStays = ui.IsShapedRunCached(font, kSize, white, kHot); - bool stayedAtCap = true; - for (std::size_t i = 0; i < cap * 2 + 1000 && hotStays; ++i) { - shapeUnique(1'000'000 + i); // fresh keys - ui.ShapeText(font, kSize, 0, 0, kHot, white, sink.data(), sink.size(), nullptr); - hotStays = ui.IsShapedRunCached(font, kSize, white, kHot); - stayedAtCap = stayedAtCap && ui.ShapedRunCacheSize() == cap; - } - Check(hotStays, "hot label survives a churn of unique strings past the cap"); - Check(stayedAtCap, "cache stays pinned at the cap (no full clear)"); - - // A cold string left untouched while the cache churns past it IS evicted — - // confirms the cap is enforced by eviction, not by refusing new inserts. - const std::string_view kCold = "Cold Once"; - ui.ShapeText(font, kSize, 0, 0, kCold, white, sink.data(), sink.size(), nullptr); - Check(ui.IsShapedRunCached(font, kSize, white, kCold), - "cold string cached when first shaped"); - for (std::size_t i = 0; i < cap + 16; ++i) shapeUnique(2'000'000 + i); // never re-touch kCold - Check(!ui.IsShapedRunCached(font, kSize, white, kCold), - "an untouched cold string is eventually evicted (LRU tail recycled)"); - if (failures == 0) std::println("\nAll ShapeText cache checks passed."); else std::println("\n{} ShapeText cache check(s) FAILED.", failures); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; diff --git a/tests/SwapchainBarrierScope/main.cpp b/tests/SwapchainBarrierScope/main.cpp deleted file mode 100644 index d200b49..0000000 --- a/tests/SwapchainBarrierScope/main.cpp +++ /dev/null @@ -1,197 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #115: the frame loop's inter-pass and frame-edge (acquire/present) -// barriers used to set BOTH stage masks to VK_PIPELINE_STAGE_ALL_COMMANDS_BIT -// and the inter-pass dependency to a queue-wide VkMemoryBarrier — flushing -// every cache and fully serialising against the whole pipeline every frame, -// when all the next pass needs is the swapchain image the previous one wrote. -// -// The fix scopes the inter-pass dependency to the swapchain image's single -// colour subresource (BuildSwapchainInterPassBarrier) and narrows the stage -// masks to the stages that actually touch that image, derived per pass via the -// polymorphic RenderPass::SwapchainStage() and unioned across the frame by -// SwapchainStageUnion. The load-bearing correctness constraint is that this -// derivation is per-pass: a compute pass writes via COMPUTE_SHADER, a ray- -// tracing pass via RAY_TRACING_SHADER, so a hardcoded compute mask would -// UNDER-synchronise an RT pass and corrupt the image. All three helpers are -// pure CPU logic over the pass list, so this test drives them directly with no -// GPU device at runtime — mirroring MipChainBarrierBatch / UploadStrategy. - -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// A pass that never overrides SwapchainStage(): stands in for any future pass -// that forgets to narrow its stage, and must fall back to the conservative -// writer union rather than silently picking one stage (which could under-sync). -struct DefaultPass : RenderPass { - void Record(GraphicsCommandBuffer, std::uint32_t, Window&) override {} -}; - -VkImage SentinelImage() { - return reinterpret_cast(static_cast(0xC0FFEE)); -} - -} // namespace - -int main() { - // ─── per-pass SwapchainStage() polymorphism ───────────────────────────── - // The whole reason the fix is "moderate, not trivial": these two stages - // MUST differ, or scoping an RT pass's barrier to the compute stage would - // under-synchronise it. Pin that they are distinct and each correct. - Check(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT != VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, - "compute and ray-tracing write stages are distinct (the under-sync trap)"); - - UIRenderer ui; // real compute UI pass - RTPass rt(nullptr); // real RT pass (SwapchainStage doesn't touch the pipeline) - DefaultPass def; // un-overridden fallback - - // Access through the base pointer — the frame loop only ever sees RenderPass*. - RenderPass* uiBase = &ui; - RenderPass* rtBase = &rt; - RenderPass* defBase = &def; - - Check(uiBase->SwapchainStage() == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - "UIRenderer reports the COMPUTE_SHADER stage"); - Check(rtBase->SwapchainStage() == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, - "RTPass reports the RAY_TRACING_SHADER stage"); - Check(defBase->SwapchainStage() == kSwapchainWriterStages, - "an un-overridden pass falls back to the conservative writer union"); - - // ─── the conservative writer union constant ───────────────────────────── - constexpr VkPipelineStageFlags expectedUnion = - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT - | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR - | VK_PIPELINE_STAGE_TRANSFER_BIT; - Check(kSwapchainWriterStages == expectedUnion, - "writer union = COMPUTE_SHADER | RAY_TRACING_SHADER | TRANSFER"); - // The point of the whole change: even the conservative fallback is far - // tighter than the ALL_COMMANDS mask it replaces. - Check(kSwapchainWriterStages != VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, - "writer union is narrower than ALL_COMMANDS"); - - // ─── SwapchainStageUnion over the frame's pass list ───────────────────── - // Empty frame: no writers to derive from, so fall back to the conservative - // union (the image is still transitioned, just never written). - Check(SwapchainStageUnion(std::span{}) == kSwapchainWriterStages, - "empty pass list falls back to the conservative writer union"); - - // All-compute frame: the union narrows to COMPUTE_SHADER ONLY — this is the - // real perf win, no RAY_TRACING / TRANSFER / ALL_COMMANDS dragged in. - { - std::array computeOnly = {uiBase, uiBase}; - VkPipelineStageFlags u = SwapchainStageUnion(computeOnly); - Check(u == VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, - "all-compute frame narrows the union to COMPUTE_SHADER only"); - Check((u & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) == 0, - "all-compute frame does NOT pull in RAY_TRACING_SHADER"); - } - - // RT-only frame: narrows to RAY_TRACING_SHADER only. - { - std::array rtOnly = {rtBase}; - Check(SwapchainStageUnion(rtOnly) == VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR, - "RT-only frame narrows the union to RAY_TRACING_SHADER only"); - } - - // Mixed frame: the union folds in BOTH stages — never under-syncs the RT - // pass, never over-syncs to ALL_COMMANDS. - { - std::array mixed = {uiBase, rtBase}; - VkPipelineStageFlags u = SwapchainStageUnion(mixed); - Check(u == (VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR), - "mixed compute+RT frame unions COMPUTE_SHADER | RAY_TRACING_SHADER"); - Check((u & VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) != 0, - "mixed frame keeps RAY_TRACING_SHADER (no RT under-sync)"); - } - - // A frame containing an un-overridden pass conservatively widens to the - // full writer union — safe by construction. - { - std::array withDefault = {uiBase, defBase}; - Check(SwapchainStageUnion(withDefault) == kSwapchainWriterStages, - "a frame with an un-overridden pass widens to the conservative union"); - } - - // ─── BuildSwapchainInterPassBarrier ───────────────────────────────────── - // Replaces the queue-wide VkMemoryBarrier: an image memory barrier scoped - // to the swapchain image's single colour subresource, GENERAL->GENERAL (no - // layout change — the storage image stays bound), matching the intra-pass - // UI barrier's access scope. - { - VkImageMemoryBarrier b = BuildSwapchainInterPassBarrier(SentinelImage()); - Check(b.sType == VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, "inter-pass barrier sType is IMAGE_MEMORY_BARRIER"); - Check(b.image == SentinelImage(), "inter-pass barrier carries the swapchain image handle"); - Check(b.oldLayout == VK_IMAGE_LAYOUT_GENERAL && b.newLayout == VK_IMAGE_LAYOUT_GENERAL, - "inter-pass barrier is GENERAL->GENERAL (pure memory dependency, no transition)"); - Check(b.srcAccessMask == VK_ACCESS_SHADER_WRITE_BIT, - "inter-pass barrier src access is shader write"); - Check(b.dstAccessMask == (VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT), - "inter-pass barrier dst access is shader read|write"); - Check(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED && b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED, - "inter-pass barrier is not a queue-family transfer"); - Check(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT, - "inter-pass barrier covers the colour aspect"); - Check(b.subresourceRange.levelCount == 1 && b.subresourceRange.layerCount == 1, - "inter-pass barrier is scoped to the single swapchain subresource"); - } - - // ─── SwapchainWriterAccess (issue #153) ───────────────────────────────── - // The frame-edge (acquire dst / present src) barriers must set an access - // mask supported by their accompanying stage mask - // (VUID-vkCmdPipelineBarrier-pImageMemoryBarriers-02820). The old code - // hardcoded SHADER_WRITE|TRANSFER_WRITE on a barrier whose stage mask is the - // per-pass union, so an all-compute frame carried TRANSFER_WRITE into a - // COMPUTE_SHADER dst stage and fired the VUID every frame. The fix derives - // the access from the same stage union, so each stage only pulls in the - // access flags it actually supports. - { - // All-compute frame: COMPUTE_SHADER supports SHADER_WRITE only — and - // crucially NOT TRANSFER_WRITE (the bit that fired the VUID). - VkAccessFlags compute = SwapchainWriterAccess(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); - Check(compute == VK_ACCESS_SHADER_WRITE_BIT, - "compute-only writer access is SHADER_WRITE only"); - Check((compute & VK_ACCESS_TRANSFER_WRITE_BIT) == 0, - "compute-only writer access does NOT include TRANSFER_WRITE (the VUID-02820 trap)"); - - // RT frame: RAY_TRACING_SHADER also writes via SHADER_WRITE, no transfer. - Check(SwapchainWriterAccess(VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR) == VK_ACCESS_SHADER_WRITE_BIT, - "RT-only writer access is SHADER_WRITE only"); - - // A transfer-stage writer pulls in TRANSFER_WRITE. - Check(SwapchainWriterAccess(VK_PIPELINE_STAGE_TRANSFER_BIT) == VK_ACCESS_TRANSFER_WRITE_BIT, - "transfer-only writer access is TRANSFER_WRITE only"); - - // The conservative writer union (un-overridden / empty frame) folds in - // both — and every bit it sets is supported by some stage in the union. - Check(SwapchainWriterAccess(kSwapchainWriterStages) - == (VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT), - "conservative writer union access is SHADER_WRITE | TRANSFER_WRITE"); - - // Mixed compute+RT (no transfer): still SHADER_WRITE only, no transfer. - VkAccessFlags mixed = SwapchainWriterAccess( - VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR); - Check(mixed == VK_ACCESS_SHADER_WRITE_BIT, - "compute+RT writer access is SHADER_WRITE only (no spurious TRANSFER_WRITE)"); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/TLASHighWaterMark/main.cpp b/tests/TLASHighWaterMark/main.cpp deleted file mode 100644 index 99174f4..0000000 --- a/tests/TLASHighWaterMark/main.cpp +++ /dev/null @@ -1,376 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #64: TLAS host-visible input buffers (instanceBuffer / metadataBuffer) -// grow on a high-water mark instead of being reallocated to the exact instance -// count on every topology change. These buffers only ever need to hold *at -// least* primitiveCount entries — the AS build reads exactly primitiveCount of -// them — so an allocation left over from a larger earlier frame is reused. -// -// This drives the real hardware path: a headless Vulkan RT device (no swapchain -// needed — a TLAS build only touches the queue + command pool), a real cube -// BLAS, and RenderingElement3D::BuildTLAS recorded into one-time command -// buffers at a sequence of instance counts. -// -// What is asserted: -// - First build at a given count ALLOCATES the host inputs (non-null handle, -// non-zero device address, size == count·sizeof(entry)). -// - Growing PAST the current capacity REALLOCATES (new VkBuffer handle, new -// address, larger size) — the only case that still pays the realloc. -// - SHRINKING reuses the existing allocation unchanged (same handle, same -// address, same size) — the core win of this issue, and the case the -// pre-fix code reallocated on. -// - Growing back up but still WITHIN the high-water capacity also reuses it. -// - Growing to EXACTLY the capacity reuses it (boundary: count > capacity, -// not >=). -// - instanceBuffer and metadataBuffer grow in lockstep (one capacity check -// governs both). -// - A same-count rebuild takes the refit (UPDATE) path: builtInstanceCount -// is unchanged and the inputs are obviously not reallocated. -// - builtInstanceCount tracks the live count across every build (the AS -// itself is still rebuilt on a count change — the fix only spares the two -// host buffers, not the AS storage/scratch). -// - The Vulkan validation layer reports ZERO errors across all of the above -// — the strongest check that feeding an oversized instance buffer to the -// AS build (with tlasRangeInfo.primitiveCount < capacity) is spec-correct. -// -// Issue #65: this test also guards the instance buffer's memory-type request — -// HOST_VISIBLE (required) with a best-effort DEVICE_LOCAL preference and no -// mandatory HOST_COHERENT, so the per-frame GPU accesses stay in local VRAM -// (BAR) rather than crossing PCIe. See the assertion block after the first -// build. The zero-validation-errors check below also covers the dropped -// HOST_COHERENT: the FlushDevice(cmd, ...) in BuildTLAS gates on the chosen -// type's coherency flag, so a non-coherent BAR type still flushes correctly. -// -// Issue #75: the metadata buffer gets the identical upgrade — it is CPU-written -// every frame and read by the ray shaders as a STORAGE_BUFFER every frame, so -// the same HOST_VISIBLE | prefer-DEVICE_LOCAL, no-mandatory-HOST_COHERENT -// request keeps those per-frame shader reads in local VRAM. A second assertion -// block after the first build checks its chosen memory type the same way. The -// dropped HOST_COHERENT is covered by the zero-validation-errors check: the -// FlushDevice() added after the copy loop in BuildTLAS makes the host writes -// available on a non-coherent type and self-gates to a no-op on a coherent one. -// -// Validation layers are required for the last check to be meaningful; the build -// marks this test as needing the SDK layers. - -#include "vulkan/vulkan.h" -#include - -import Crafter.Graphics; -import Crafter.Math; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -// One-time command buffer helpers — record a build, submit, block. -VkCommandBuffer BeginCmd() { - VkCommandBufferAllocateInfo allocInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .commandPool = Device::commandPool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1, - }; - VkCommandBuffer cmd = VK_NULL_HANDLE; - Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd)); - VkCommandBufferBeginInfo beginInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - }; - Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo)); - return cmd; -} - -void SubmitWait(VkCommandBuffer cmd) { - Device::CheckVkResult(vkEndCommandBuffer(cmd)); - VkSubmitInfo submitInfo { - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .commandBufferCount = 1, - .pCommandBuffers = &cmd, - }; - Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); -} - -// A unit cube (8 verts, 12 triangles) — enough topology for a real BLAS. -std::vector> CubeVerts(float s) { - return { - {-s,-s,-s}, { s,-s,-s}, { s, s,-s}, {-s, s,-s}, - {-s,-s, s}, { s,-s, s}, { s, s, s}, {-s, s, s}, - }; -} -std::vector CubeIndices() { - return { - 0,1,2, 0,2,3, 4,6,5, 4,7,6, - 0,4,5, 0,5,1, 3,2,6, 3,6,7, - 1,5,6, 1,6,2, 0,3,7, 0,7,4, - }; -} - -// One TLAS instance referencing `blasAddr`, identity transform, visible mask. -VkAccelerationStructureInstanceKHR MakeInstance(VkDeviceAddress blasAddr) { - VkAccelerationStructureInstanceKHR inst{}; - inst.transform = VkTransformMatrixKHR{{ - {1.0f, 0.0f, 0.0f, 0.0f}, - {0.0f, 1.0f, 0.0f, 0.0f}, - {0.0f, 0.0f, 1.0f, 0.0f}, - }}; - inst.mask = 0xFF; - inst.accelerationStructureReference = blasAddr; - return inst; -} - -// Register exactly `n` elements from a stable (reserved, never-reallocated) -// pool. Clears the current registration first so the live count is exactly n. -void SetCount(std::vector& pool, std::uint32_t n) { - while (!RenderingElement3D::elements.empty()) { - RenderingElement3D::Remove(RenderingElement3D::elements.back()); - } - for (std::uint32_t i = 0; i < n; ++i) { - RenderingElement3D::Add(&pool[i]); - } -} - -// Build the frame-0 TLAS for the currently-registered elements. -void BuildOnce() { - VkCommandBuffer cmd = BeginCmd(); - RenderingElement3D::BuildTLAS(cmd, 0); - SubmitWait(cmd); -} - -// Snapshot of the host-input buffer identity, for before/after comparison. -struct InputSnapshot { - VkBuffer instBuf; - VkDeviceAddress instAddr; - std::uint32_t instSize; - VkBuffer metaBuf; - VkDeviceAddress metaAddr; - std::uint32_t metaSize; -}; -InputSnapshot Snapshot() { - auto& tlas = RenderingElement3D::tlases[0]; - return { - tlas.instanceBuffer.buffer, tlas.instanceBuffer.address, tlas.instanceBuffer.size, - tlas.metadataBuffer.buffer, tlas.metadataBuffer.address, tlas.metadataBuffer.size, - }; -} - -// Both host inputs untouched (same VkBuffer handle, address and size). -bool Reused(const InputSnapshot& a, const InputSnapshot& b) { - return a.instBuf == b.instBuf && a.instAddr == b.instAddr && a.instSize == b.instSize - && a.metaBuf == b.metaBuf && a.metaAddr == b.metaAddr && a.metaSize == b.metaSize; -} - -constexpr std::uint32_t kEntrySize = sizeof(VkAccelerationStructureInstanceKHR); - -} // namespace - -int main() { - Device::Initialize(); - Device::validationErrorCount = 0; - - // One real cube BLAS that every TLAS instance references. - Mesh cube; - { - auto verts = CubeVerts(1.0f); - auto idx = CubeIndices(); - VkCommandBuffer cmd = BeginCmd(); - cube.Build(verts, idx, cmd, RTBuildOptions{ .allowUpdate = true }); - SubmitWait(cmd); - } - Check(cube.blasAddr != 0, "cube BLAS produced a non-zero blasAddr"); - - // Stable backing store for the elements. Reserve to the max count this - // test ever registers so the vector never reallocates (the static - // `elements` array holds raw pointers into it). - constexpr std::uint32_t kMaxElems = 32; - std::vector pool(kMaxElems); - for (auto& e : pool) e.instance = MakeInstance(cube.blasAddr); - - // ── 1. First build at 4 instances → allocates the host inputs. ────────── - SetCount(pool, 4); - BuildOnce(); - InputSnapshot s4 = Snapshot(); - Check(s4.instBuf != VK_NULL_HANDLE, "first build allocated the instance buffer"); - Check(s4.metaBuf != VK_NULL_HANDLE, "first build allocated the metadata buffer"); - Check(s4.instAddr != 0, "instance buffer has a non-zero device address"); - Check(s4.instSize == 4 * kEntrySize, "instance buffer sized for 4 entries"); - Check(RenderingElement3D::tlases[0].builtInstanceCount == 4, - "builtInstanceCount == 4 after first build"); - - // ── Issue #65: the instance buffer is allocated HOST_VISIBLE (required, - // mappable) with a best-effort DEVICE_LOCAL preference (BAR/VRAM) and the - // mandatory HOST_COHERENT dropped — so the per-frame compute-write of the - // transform field and the AS build's read of the instances both stay in - // local VRAM instead of crossing PCIe. The chosen type must match exactly - // what GetMemoryType resolves for this buffer's memoryTypeBits with that - // prefer-but-don't-require request: DEVICE_LOCAL when a host-visible - // device-local type is reachable (resizable BAR), plain HOST_VISIBLE - // otherwise. Asserting equality with the recomputed selection makes the - // check device-independent — it tracks the call-site request, not the - // particular GPU this runs on. ──────────────────────────────────────────── - { - auto& ib = RenderingElement3D::tlases[0].instanceBuffer; - VkMemoryRequirements req{}; - vkGetBufferMemoryRequirements(Device::device, ib.buffer, &req); - std::uint32_t expectIdx = Device::GetMemoryType( - req.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VkMemoryPropertyFlags expectFlags = - Device::memoryProperties.memoryTypes[expectIdx].propertyFlags; - Check((ib.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0, - "instance buffer memory is HOST_VISIBLE (mappable — the required flag)"); - Check(ib.memoryPropertyFlagsChosen == expectFlags, - "instance buffer memory matches the HOST_VISIBLE | prefer-DEVICE_LOCAL request (#65)"); - - bool barReachable = false; - constexpr VkMemoryPropertyFlags kBar = - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; - for (std::uint32_t i = 0; i < Device::memoryProperties.memoryTypeCount; ++i) { - if ((req.memoryTypeBits & (1u << i)) - && (Device::memoryProperties.memoryTypes[i].propertyFlags & kBar) == kBar) { - barReachable = true; - break; - } - } - if (barReachable) { - Check((ib.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0, - "instance buffer landed on a DEVICE_LOCAL (BAR/VRAM) type where one is reachable"); - } else { - std::println("INFO no host-visible device-local type reachable — " - "fell back to plain HOST_VISIBLE (expected on non-BAR GPUs)"); - } - } - - // ── Issue #75: the metadata buffer carries the same memory-type request as - // the instance buffer — HOST_VISIBLE (required, mappable) with a best-effort - // DEVICE_LOCAL preference (BAR/VRAM) and the mandatory HOST_COHERENT dropped. - // It is CPU-written every frame and read by the ray shaders every frame, so - // landing it in local VRAM keeps those per-frame storage-buffer reads off - // PCIe. Same device-independent check: assert the recorded chosen flags equal - // what GetMemoryType resolves for this buffer's memoryTypeBits with that - // request, and that DEVICE_LOCAL is chosen wherever a host-visible - // device-local type is reachable. ─────────────────────────────────────────── - { - auto& mb = RenderingElement3D::tlases[0].metadataBuffer; - VkMemoryRequirements req{}; - vkGetBufferMemoryRequirements(Device::device, mb.buffer, &req); - std::uint32_t expectIdx = Device::GetMemoryType( - req.memoryTypeBits, - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, - VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - VkMemoryPropertyFlags expectFlags = - Device::memoryProperties.memoryTypes[expectIdx].propertyFlags; - Check((mb.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0, - "metadata buffer memory is HOST_VISIBLE (mappable — the required flag)"); - Check(mb.memoryPropertyFlagsChosen == expectFlags, - "metadata buffer memory matches the HOST_VISIBLE | prefer-DEVICE_LOCAL request (#75)"); - - bool barReachable = false; - constexpr VkMemoryPropertyFlags kBar = - VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; - for (std::uint32_t i = 0; i < Device::memoryProperties.memoryTypeCount; ++i) { - if ((req.memoryTypeBits & (1u << i)) - && (Device::memoryProperties.memoryTypes[i].propertyFlags & kBar) == kBar) { - barReachable = true; - break; - } - } - if (barReachable) { - Check((mb.memoryPropertyFlagsChosen & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0, - "metadata buffer landed on a DEVICE_LOCAL (BAR/VRAM) type where one is reachable"); - } else { - std::println("INFO no host-visible device-local type reachable — " - "metadata fell back to plain HOST_VISIBLE (expected on non-BAR GPUs)"); - } - } - - // ── 2. Grow to 16 (past capacity) → REALLOCATES. ──────────────────────── - SetCount(pool, 16); - BuildOnce(); - InputSnapshot s16 = Snapshot(); - Check(s16.instBuf != s4.instBuf, "growing past capacity reallocated the instance buffer"); - Check(s16.metaBuf != s4.metaBuf, "growing past capacity reallocated the metadata buffer"); - Check(s16.instSize == 16 * kEntrySize, "instance buffer grew to 16 entries"); - Check(RenderingElement3D::tlases[0].builtInstanceCount == 16, - "builtInstanceCount == 16 after growth"); - - // ── 3. Shrink to 2 → REUSES the 16-entry allocation (the core win). ───── - SetCount(pool, 2); - BuildOnce(); - InputSnapshot s2 = Snapshot(); - Check(Reused(s16, s2), - "shrinking to 2 reused the existing buffers (no realloc — high-water mark)"); - Check(s2.instSize == 16 * kEntrySize, - "instance buffer kept its 16-entry high-water size after shrink"); - Check(RenderingElement3D::tlases[0].builtInstanceCount == 2, - "builtInstanceCount tracks the live count (2) even on the reuse path"); - - // ── 4. Grow to 10 (still within the high-water capacity) → REUSES. ────── - SetCount(pool, 10); - BuildOnce(); - InputSnapshot s10 = Snapshot(); - Check(Reused(s16, s10), - "growing to 10 (≤ capacity 16) reused the existing buffers"); - - // ── 5. Grow to exactly 16 (== capacity) → REUSES (boundary: > not >=). ── - SetCount(pool, 16); - BuildOnce(); - InputSnapshot s16b = Snapshot(); - Check(Reused(s16, s16b), - "growing to exactly the capacity (16) reused the existing buffers"); - - // ── 6. Same count again → refit (UPDATE) path, inputs untouched. ──────── - BuildOnce(); - InputSnapshot s16c = Snapshot(); - Check(Reused(s16, s16c), "same-count rebuild left the inputs untouched"); - Check(RenderingElement3D::tlases[0].builtInstanceCount == 16, - "same-count rebuild kept builtInstanceCount == 16 (took the refit path)"); - - // ── 7. Grow to 32 (past the high-water) → REALLOCATES again. ──────────── - SetCount(pool, 32); - BuildOnce(); - InputSnapshot s32 = Snapshot(); - Check(s32.instBuf != s16.instBuf, "growing past the high-water (32) reallocated again"); - Check(s32.instSize == 32 * kEntrySize, "instance buffer grew to 32 entries"); - - // Unregister everything so nothing dangles past the pool's lifetime. - SetCount(pool, 0); - - Check(Device::validationErrorCount == 0, - std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); - - // Tear down the frame-0 TLAS while the Vulkan device is still alive. The - // static tlases[] array is destroyed at process exit, by which point the - // device (also static) may already be gone — so its VulkanBuffer - // destructors would fault. Releasing here keeps the exit path clean. - { - auto& t0 = RenderingElement3D::tlases[0]; - if (t0.accelerationStructure != VK_NULL_HANDLE) { - Device::vkDestroyAccelerationStructureKHR(Device::device, t0.accelerationStructure, nullptr); - t0.accelerationStructure = VK_NULL_HANDLE; - } - if (t0.instanceBuffer.buffer != VK_NULL_HANDLE) t0.instanceBuffer.Clear(); - if (t0.metadataBuffer.buffer != VK_NULL_HANDLE) t0.metadataBuffer.Clear(); - if (t0.scratchBuffer.buffer != VK_NULL_HANDLE) t0.scratchBuffer.Clear(); - if (t0.buffer.buffer != VK_NULL_HANDLE) t0.buffer.Clear(); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/TLASInstanceDirtyTracking/main.cpp b/tests/TLASInstanceDirtyTracking/main.cpp deleted file mode 100644 index 4ba711a..0000000 --- a/tests/TLASInstanceDirtyTracking/main.cpp +++ /dev/null @@ -1,236 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Issue #118: the per-frame TLAS instance+metadata host rebuild no longer copies -// every entry unconditionally — it copies (and flushes) only the slots whose -// element's host-authored data changed since this frame's buffers last saw it, -// tracked by RenderingElement3D::hostDataVersion against the per-frame -// TlasWithBuffer::uploadedVersion record. Elements left at version 0 stay -// "untracked" and are copied every frame (the pre-#118 behaviour), so callers -// that don't opt in keep working; MarkHostDataDirty() opts an element in. -// -// This drives the real hardware path (a headless Vulkan RT device, a real cube -// BLAS, RenderingElement3D::BuildTLAS into one-time command buffers) exactly as -// TLASHighWaterMark does, and asserts behaviour by reading back the host-mapped -// instanceBuffer/metadataBuffer (host-visible, and we only inspect host-written -// fields, so the mapping reflects what the copy loop wrote — a skipped slot -// retains its previous bytes, which is the whole point). -// -// What is asserted: -// - A tracked (MarkHostDataDirty'd) element is uploaded on first build. -// - Mutating a tracked element's host data WITHOUT marking it dirty and -// rebuilding leaves the buffer holding the OLD value — the slot was skipped. -// - Marking it dirty and rebuilding uploads the NEW value. -// - An untracked element (version 0) is uploaded on every build even without -// a mark — the backward-compatible always-copy path. -// - Relocation on the refit path: a remove+add that nets the same instance -// count (so BuildTLAS refits rather than rebuilding) re-uploads exactly the -// slots whose occupying element changed, and leaves the unchanged slots -// intact — proving globally-unique versions handle swap-and-pop without -// tracking element identity. -// - The Vulkan validation layer reports ZERO errors across all of the above — -// covering the ranged FlushDevice(offset, bytes) the dirty span feeds. - -#include "vulkan/vulkan.h" -#include - -import Crafter.Graphics; -import Crafter.Math; -import std; - -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -VkCommandBuffer BeginCmd() { - VkCommandBufferAllocateInfo allocInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - .commandPool = Device::commandPool, - .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, - .commandBufferCount = 1, - }; - VkCommandBuffer cmd = VK_NULL_HANDLE; - Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd)); - VkCommandBufferBeginInfo beginInfo { - .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - }; - Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo)); - return cmd; -} - -void SubmitWait(VkCommandBuffer cmd) { - Device::CheckVkResult(vkEndCommandBuffer(cmd)); - VkSubmitInfo submitInfo { - .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, - .commandBufferCount = 1, - .pCommandBuffers = &cmd, - }; - Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE)); - Device::CheckVkResult(vkQueueWaitIdle(Device::queue)); - vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd); -} - -std::vector> CubeVerts(float s) { - return { - {-s,-s,-s}, { s,-s,-s}, { s, s,-s}, {-s, s,-s}, - {-s,-s, s}, { s,-s, s}, { s, s, s}, {-s, s, s}, - }; -} -std::vector CubeIndices() { - return { - 0,1,2, 0,2,3, 4,6,5, 4,7,6, - 0,4,5, 0,5,1, 3,2,6, 3,6,7, - 1,5,6, 1,6,2, 0,3,7, 0,7,4, - }; -} - -VkAccelerationStructureInstanceKHR MakeInstance(VkDeviceAddress blasAddr) { - VkAccelerationStructureInstanceKHR inst{}; - inst.transform = VkTransformMatrixKHR{{ - {1.0f, 0.0f, 0.0f, 0.0f}, - {0.0f, 1.0f, 0.0f, 0.0f}, - {0.0f, 0.0f, 1.0f, 0.0f}, - }}; - inst.mask = 0xFF; - inst.accelerationStructureReference = blasAddr; - return inst; -} - -void BuildOnce() { - VkCommandBuffer cmd = BeginCmd(); - RenderingElement3D::BuildTLAS(cmd, 0); - SubmitWait(cmd); -} - -// Read back what the host copy loop last wrote into frame-0's metadata buffer -// slot. Host-visible and host-written, so the mapping reflects the last copy -// (or the slot's prior contents if it was skipped) without an invalidate. -std::uint32_t MetaSlot(std::uint32_t i) { - return RenderingElement3D::tlases[0].metadataBuffer.value[i]; -} -std::uint32_t CustomIndexSlot(std::uint32_t i) { - return RenderingElement3D::tlases[0].instanceBuffer.value[i].instanceCustomIndex; -} - -} // namespace - -int main() { - Device::Initialize(); - Device::validationErrorCount = 0; - - Mesh cube; - { - auto verts = CubeVerts(1.0f); - auto idx = CubeIndices(); - VkCommandBuffer cmd = BeginCmd(); - cube.Build(verts, idx, cmd, RTBuildOptions{ .allowUpdate = true }); - SubmitWait(cmd); - } - Check(cube.blasAddr != 0, "cube BLAS produced a non-zero blasAddr"); - - constexpr std::uint32_t kMaxElems = 8; - std::vector pool(kMaxElems); - for (auto& e : pool) e.instance = MakeInstance(cube.blasAddr); - - // ── 1. A tracked element is uploaded on first build. ──────────────────── - pool[0].userMetadata = 100; - pool[0].instance.instanceCustomIndex = 100; - pool[0].MarkHostDataDirty(); - RenderingElement3D::Add(&pool[0]); - BuildOnce(); - Check(MetaSlot(0) == 100, "tracked element uploaded on first build (metadata)"); - Check(CustomIndexSlot(0) == 100, "tracked element uploaded on first build (customIndex)"); - - // ── 2. Mutating a tracked element WITHOUT marking it dirty and rebuilding - // leaves the old value — the slot is skipped. The instance count is - // unchanged, so this is the refit path (no topology reset). ───────── - pool[0].userMetadata = 200; - pool[0].instance.instanceCustomIndex = 200; - BuildOnce(); - Check(MetaSlot(0) == 100, - "unmarked mutation is NOT uploaded — clean slot skipped (metadata stays 100)"); - Check(CustomIndexSlot(0) == 100, - "unmarked mutation is NOT uploaded — clean slot skipped (customIndex stays 100)"); - - // ── 3. Marking it dirty and rebuilding uploads the new value. ─────────── - pool[0].MarkHostDataDirty(); - BuildOnce(); - Check(MetaSlot(0) == 200, "marking dirty re-uploads the new metadata (200)"); - Check(CustomIndexSlot(0) == 200, "marking dirty re-uploads the new customIndex (200)"); - - // ── 4. An untracked element (version 0) is uploaded every build even - // without a mark — the backward-compatible always-copy path. Add a - // second element to grow the count (topology change), then mutate it - // in place across same-count rebuilds. ───────────────────────────── - pool[1].userMetadata = 300; // left untracked: hostDataVersion == 0 - RenderingElement3D::Add(&pool[1]); - BuildOnce(); - Check(MetaSlot(1) == 300, "untracked element uploaded on first build (300)"); - pool[1].userMetadata = 400; // mutate in place, no MarkHostDataDirty - BuildOnce(); - Check(MetaSlot(1) == 400, - "untracked element re-uploaded every frame without a mark (400) — legacy path"); - // The tracked element [0] must NOT have been disturbed by [1]'s churn. - Check(MetaSlot(0) == 200, "tracked element [0] untouched while [1] churns (still 200)"); - - // ── 5. Relocation on the refit path. Both live elements are tracked; a - // remove+add that nets the same instance count takes the refit path - // (no topology reset), so only the slots whose occupying element - // changed must be re-uploaded — proving globally-unique versions - // handle swap-and-pop without tracking element identity. ─────────── - // Mark both current elements so neither is the always-copy legacy path. - pool[0].userMetadata = 10; pool[0].MarkHostDataDirty(); - pool[1].userMetadata = 11; pool[1].MarkHostDataDirty(); - BuildOnce(); // slots: [0]=10, [1]=11 - Check(MetaSlot(0) == 10 && MetaSlot(1) == 11, "both tracked elements uploaded (10, 11)"); - - // Remove pool[0] (swap-and-pop moves the last element, pool[1], into slot 0), - // then add pool[2] (appended at slot 1). Net count is unchanged at 2, so - // BuildTLAS refits. New mapping: slot 0 -> pool[1] (was pool[0]), - // slot 1 -> pool[2] (was pool[1]). - pool[2].userMetadata = 22; pool[2].MarkHostDataDirty(); - RenderingElement3D::Remove(&pool[0]); - RenderingElement3D::Add(&pool[2]); - BuildOnce(); - Check(MetaSlot(0) == 11, - "relocation: slot 0 re-uploaded to the swapped-in element (pool[1] -> 11)"); - Check(MetaSlot(1) == 22, - "relocation: slot 1 re-uploaded to the appended element (pool[2] -> 22)"); - - // Unregister everything. - while (!RenderingElement3D::elements.empty()) { - RenderingElement3D::Remove(RenderingElement3D::elements.back()); - } - - Check(Device::validationErrorCount == 0, - std::format("no Vulkan validation errors ({} seen)", Device::validationErrorCount)); - - // Tear down the frame-0 TLAS while the device is still alive (same rationale - // as TLASHighWaterMark — the static tlases[] outlive the static device). - { - auto& t0 = RenderingElement3D::tlases[0]; - if (t0.accelerationStructure != VK_NULL_HANDLE) { - Device::vkDestroyAccelerationStructureKHR(Device::device, t0.accelerationStructure, nullptr); - t0.accelerationStructure = VK_NULL_HANDLE; - } - if (t0.instanceBuffer.buffer != VK_NULL_HANDLE) t0.instanceBuffer.Clear(); - if (t0.metadataBuffer.buffer != VK_NULL_HANDLE) t0.metadataBuffer.Clear(); - if (t0.scratchBuffer.buffer != VK_NULL_HANDLE) t0.scratchBuffer.Clear(); - if (t0.buffer.buffer != VK_NULL_HANDLE) t0.buffer.Clear(); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/UIClipFlag/main.cpp b/tests/UIClipFlag/main.cpp deleted file mode 100644 index cef9c4d..0000000 --- a/tests/UIClipFlag/main.cpp +++ /dev/null @@ -1,89 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #50: uiResolveScreenPixel used to run four float -// compares against hdr.clipRectPx for every pixel of every dispatch, even -// though the clip rect is the full-surface default {0,0,1e9,1e9} in the -// overwhelmingly common case. The compares are now gated on the reserved -// kUIFlagClip bit, which UIRenderer::FillHeader sets (via ClipFlags) only when -// the clip rect is actually narrower than the surface. -// -// ClipFlags is the pure CPU decision behind that gate: given a clip rect, the -// surface size, and the caller's user flags, it returns the header flag word -// with kUIFlagClip set iff the rect does not cover the whole surface. This -// test drives it directly with synthetic dimensions — no Window or device. -// -// The contract it must hold for the shader gate to be correct: -// * full-surface (and over-covering) clips must NOT set the bit, so the -// shader skips the compares — and the skipped compares would never have -// rejected an in-surface pixel anyway, so output is pixel-identical; -// * any clip narrower than the surface on ANY edge MUST set the bit; -// * the caller's user flag bits are preserved either way; only the reserved -// high bit is touched. - -#include - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -constexpr std::array kFullDefault = {0.0f, 0.0f, 1e9f, 1e9f}; - -bool ClipBitSet(std::array rect, std::uint32_t w, std::uint32_t h, - std::uint32_t userFlags = 0) { - return (UIRenderer::ClipFlags(rect, w, h, userFlags) & kUIFlagClip) != 0u; -} - -} // namespace - -int main() { - constexpr std::uint32_t W = 1920, H = 1080; - - // --- full-surface clips leave the bit clear (the fast path) ----------- - Check(!ClipBitSet(kFullDefault, W, H), - "the {0,0,1e9,1e9} default does not set the clip bit"); - Check(!ClipBitSet({0.0f, 0.0f, float(W), float(H)}, W, H), - "a clip exactly matching the surface does not set the clip bit"); - Check(!ClipBitSet({-5.0f, -5.0f, float(W) + 10.0f, float(H) + 10.0f}, W, H), - "a clip that over-covers the surface does not set the clip bit"); - - // --- any narrower edge sets the bit ----------------------------------- - Check(ClipBitSet({0.0f, 0.0f, float(W) - 1.0f, float(H)}, W, H), - "a clip one pixel short on the right edge sets the clip bit"); - Check(ClipBitSet({0.0f, 0.0f, float(W), float(H) - 1.0f}, W, H), - "a clip one pixel short on the bottom edge sets the clip bit"); - Check(ClipBitSet({1.0f, 0.0f, float(W), float(H)}, W, H), - "a clip inset on the left edge sets the clip bit"); - Check(ClipBitSet({0.0f, 1.0f, float(W), float(H)}, W, H), - "a clip inset on the top edge sets the clip bit"); - Check(ClipBitSet({100.0f, 100.0f, 200.0f, 200.0f}, W, H), - "a small interior clip rect sets the clip bit"); - - // --- user flag bits survive the decision ------------------------------ - constexpr std::uint32_t kUser = 0x0000000Fu; // four low user bits - Check(UIRenderer::ClipFlags(kFullDefault, W, H, kUser) == kUser, - "full-surface clip preserves user flags and clears the reserved bit"); - Check(UIRenderer::ClipFlags({0.0f, 0.0f, 200.0f, 200.0f}, W, H, kUser) - == (kUser | kUIFlagClip), - "narrow clip preserves user flags and sets the reserved bit"); - // A caller that (wrongly) pre-set the reserved bit must not flip the - // decision for a full-surface clip — ClipFlags owns that bit. - Check(UIRenderer::ClipFlags(kFullDefault, W, H, kUIFlagClip | kUser) == kUser, - "full-surface clip clears a stale reserved bit the caller passed in"); - - if (failures != 0) { - std::println(std::cerr, "{} case(s) failed", failures); - return 1; - } - std::println(std::cout, "all UI clip-flag cases passed"); - return 0; -} diff --git a/tests/UIFusedShader/main.cpp b/tests/UIFusedShader/main.cpp deleted file mode 100644 index 9d4f1c3..0000000 --- a/tests/UIFusedShader/main.cpp +++ /dev/null @@ -1,187 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #47: the fused UI uber-kernel -// (shaders/ui-fused.comp.glsl) and its C++ push-constant mirror -// (Crafter::UIFusedHeader). The fused kernel is large and hand-written, and -// its push-constant block must stay byte-for-byte identical to UIFusedHeader -// or DispatchFused silently corrupts every fused frame. This test: -// -// 1. compiles the real ui-fused.comp.glsl with glslang (same target-env the -// engine builds it with), proving the uber-kernel still compiles and its -// GL_GOOGLE_include of ui-shared.glsl resolves; -// 2. runs spirv-val over the result under the engine's block-layout flags; -// 3. asserts exactly one push-constant variable survives (a well-formed -// single block, like every other UI shader); -// 4. parses the push-constant struct's member Offset decorations and asserts -// they are exactly {0,16,32,48,64,80,96,112} — the layout of -// UIFusedHeader (four uvec4 headers at 0/16/32/48 then four vec4 clip -// rects at 64/80/96/112, 128 bytes total). Any add/remove/reorder of a -// member shifts these offsets and fails here, catching a GLSL/C++ drift -// that the C++-side static_assert(sizeof==128) alone cannot. -// -// glslang and spirv-val are invoked at runtime, so the test declares them as -// required tools (see project.cpp). It needs no GPU device. - -#include - -import Crafter.Graphics; // for the static_assert(sizeof(UIFusedHeader)==128) -import std; -using namespace Crafter; - -namespace { - -namespace fs = std::filesystem; - -int RunCommand(const std::string& cmd) { - int status = std::system(cmd.c_str()); - if (status == -1) return -1; - return (status & 0x7f) == 0 ? ((status >> 8) & 0xff) : 128 + (status & 0x7f); -} - -std::vector ReadSpirv(const fs::path& p) { - std::ifstream f(p, std::ios::binary | std::ios::ate); - if (!f) return {}; - std::streamsize size = f.tellg(); - f.seekg(0); - std::vector words(static_cast(size) / sizeof(std::uint32_t)); - f.read(reinterpret_cast(words.data()), size); - return words; -} - -// Count OpVariable instructions in the PushConstant storage class (SC == 9). -int CountPushConstantVariables(const std::vector& words) { - constexpr std::uint32_t OpVariable = 59; - constexpr std::uint32_t StorageClassPushConstant = 9; - int count = 0; - for (std::size_t i = 5; i < words.size();) { - std::uint32_t len = words[i] >> 16; - if (len == 0 || i + len > words.size()) break; - if ((words[i] & 0xFFFFu) == OpVariable && len >= 4 && words[i + 3] == StorageClassPushConstant) - ++count; - i += len; - } - return count; -} - -// The single PushConstant struct's member byte offsets, sorted ascending. -// Finds the OpTypePointer in the PushConstant storage class (SC == 9), takes -// the struct type it points at, and collects every Offset (decoration 35) -// declared on that struct via OpMemberDecorate. -std::vector PushConstantMemberOffsets(const std::vector& words) { - constexpr std::uint32_t OpTypePointer = 32; - constexpr std::uint32_t OpMemberDecorate = 72; - constexpr std::uint32_t StorageClassPushConstant = 9; - constexpr std::uint32_t DecorationOffset = 35; - - std::uint32_t structId = 0; - for (std::size_t i = 5; i < words.size();) { - std::uint32_t len = words[i] >> 16; - if (len == 0 || i + len > words.size()) break; - // OpTypePointer: [op][result id][storage class][pointee type id] - if ((words[i] & 0xFFFFu) == OpTypePointer && len >= 4 && - words[i + 2] == StorageClassPushConstant) { - structId = words[i + 3]; - break; - } - i += len; - } - if (structId == 0) return {}; - - std::vector offsets; - for (std::size_t i = 5; i < words.size();) { - std::uint32_t len = words[i] >> 16; - if (len == 0 || i + len > words.size()) break; - // OpMemberDecorate: [op][struct id][member][decoration][operands...] - if ((words[i] & 0xFFFFu) == OpMemberDecorate && len >= 5 && - words[i + 1] == structId && words[i + 3] == DecorationOffset) { - offsets.push_back(words[i + 4]); - } - i += len; - } - std::ranges::sort(offsets); - offsets.erase(std::ranges::unique(offsets).begin(), offsets.end()); - return offsets; -} - -// Locate shaders/ui-fused.comp.glsl whether the test runs from the build -// output dir or the project root (mirrors ShapeTextCache's font probing). -fs::path LocateShader() { - const std::array candidates = { - "shaders/ui-fused.comp.glsl", - "../shaders/ui-fused.comp.glsl", - "../../shaders/ui-fused.comp.glsl", - "../../../shaders/ui-fused.comp.glsl", - }; - for (const auto& c : candidates) { - std::error_code ec; - if (fs::exists(c, ec)) return c; - } - return {}; -} - -} // namespace - -int main() { - // Sanity: the C++ mirror is the size the shader layout assumes. - static_assert(sizeof(UIFusedHeader) == 128); - - const fs::path src = LocateShader(); - if (src.empty()) { - std::println(std::cerr, "could not locate shaders/ui-fused.comp.glsl from {}", - fs::current_path().string()); - return 1; - } - - const fs::path dir = fs::temp_directory_path() / "crafter-uifused-test"; - std::error_code ec; - fs::create_directories(dir, ec); - const fs::path spv = dir / "ui-fused.comp.spv"; - - // 1. Compile the real uber-kernel. GL_GOOGLE_include resolves - // ui-shared.glsl relative to the source directory. - std::string compile = "glslang --target-env vulkan1.4 -V -S comp \"" - + src.string() + "\" -o \"" + spv.string() + "\" > /dev/null"; - if (RunCommand(compile) != 0) { - std::println(std::cerr, "glslang failed to compile ui-fused.comp.glsl"); - return 1; - } - - std::vector words = ReadSpirv(spv); - if (words.size() < 5) { - std::println(std::cerr, "could not read compiled SPIR-V"); - return 1; - } - - // 2. spirv-val under the engine's relaxed block-layout flags. - std::string validate = "spirv-val \"" + spv.string() - + "\" --relax-block-layout --scalar-block-layout --target-env vulkan1.4"; - if (RunCommand(validate) != 0) { - std::println(std::cerr, "spirv-val rejected ui-fused.comp.spv"); - return 1; - } - - // 3. Exactly one push-constant block. - int pcVars = CountPushConstantVariables(words); - if (pcVars != 1) { - std::println(std::cerr, "expected exactly 1 push-constant variable, found {}", pcVars); - return 1; - } - - // 4. Member offsets must match UIFusedHeader's layout exactly. - const std::vector expected = {0, 16, 32, 48, 64, 80, 96, 112}; - std::vector offsets = PushConstantMemberOffsets(words); - if (offsets != expected) { - std::print(std::cerr, "push-constant member offsets mismatch — expected {{"); - for (auto o : expected) std::print(std::cerr, "{} ", o); - std::print(std::cerr, "}}, got {{"); - for (auto o : offsets) std::print(std::cerr, "{} ", o); - std::println(std::cerr, "}}"); - return 1; - } - - std::println(std::cout, - "ui-fused shader ok (push-constant vars: {}, member offsets pinned to UIFusedHeader, 128 bytes)", - pcVars); - return 0; -} diff --git a/tests/UploadStrategy/main.cpp b/tests/UploadStrategy/main.cpp deleted file mode 100644 index aea9cf4..0000000 --- a/tests/UploadStrategy/main.cpp +++ /dev/null @@ -1,161 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #89: Device::PreferDirectDeviceWrite is the runtime -// upload-strategy helper that decides whether a CPU-written, GPU-read buffer -// should be written directly into a HOST_VISIBLE | DEVICE_LOCAL allocation -// (true) or staged through a HOST_VISIBLE buffer + vkCmdCopyBuffer into pure -// DEVICE_LOCAL (false). The choice is platform-dependent: -// - ReBAR / UMA: the host-visible device-local heap is ~all of VRAM, so -// staging is pure overhead -> direct. -// - non-ReBAR discrete: HOST_VISIBLE | DEVICE_LOCAL is a small BAR window on -// a separate heap; large buffers must stage (#58), small/hot ones may map. -// - no host-visible device-local type at all -> staging is mandatory. -// -// The decision is pure CPU logic over Device::memoryProperties (both the -// memory types and the heaps), so this test installs synthetic layouts and -// drives it directly — no GPU device needed at runtime, mirroring -// MemoryTypeFallback. - -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; -constexpr auto HOST_VISIBLE = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; -constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; -constexpr auto HOST_CACHED = VK_MEMORY_PROPERTY_HOST_CACHED_BIT; - -constexpr VkDeviceSize KiB = 1024; -constexpr VkDeviceSize MiB = 1024 * KiB; -constexpr VkDeviceSize GiB = 1024 * MiB; - -struct TypeSpec { VkMemoryPropertyFlags flags; std::uint32_t heapIndex; }; -struct HeapSpec { VkDeviceSize size; VkMemoryHeapFlags flags; }; - -// Install a synthetic memory layout: explicit types (with their heap index) -// and explicit heaps (size + flags). -void SetMemory(std::initializer_list types, - std::initializer_list heaps) { - Device::memoryProperties = {}; - Device::memoryProperties.memoryTypeCount = static_cast(types.size()); - std::uint32_t i = 0; - for (const TypeSpec& t : types) { - Device::memoryProperties.memoryTypes[i].propertyFlags = t.flags; - Device::memoryProperties.memoryTypes[i].heapIndex = t.heapIndex; - ++i; - } - Device::memoryProperties.memoryHeapCount = static_cast(heaps.size()); - std::uint32_t h = 0; - for (const HeapSpec& s : heaps) { - Device::memoryProperties.memoryHeaps[h].size = s.size; - Device::memoryProperties.memoryHeaps[h].flags = s.flags; - ++h; - } - // PreferDirectDeviceWrite reads the cache CacheUploadStrategy derives from - // memoryProperties (device creation does this once); refresh it here since - // the test mutates the layout directly. - Device::CacheUploadStrategy(); -} - -constexpr VkMemoryHeapFlags HEAP_DEVICE_LOCAL = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; - -} // namespace - -int main() { - // --- ReBAR: full 24 GiB heap is DEVICE_LOCAL | HOST_VISIBLE (this dev - // machine, type 4 on heap 0) -> always direct, even for huge buffers -- - { - SetMemory( - { {DEVICE_LOCAL, 0}, - {HOST_VISIBLE | HOST_COHERENT, 1}, - {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 0} }, - { {24 * GiB, HEAP_DEVICE_LOCAL}, {32 * GiB, 0} }); - Check(Device::PreferDirectDeviceWrite(8), - "ReBAR: a tiny buffer maps directly"); - Check(Device::PreferDirectDeviceWrite(8 * GiB), - "ReBAR: even a multi-GiB buffer maps directly (no overhead staging)"); - } - - // --- UMA: a single heap with all flags -> direct ---------------------- - { - SetMemory( - { {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT | HOST_CACHED, 0} }, - { {125 * GiB, HEAP_DEVICE_LOCAL} }); - Check(Device::PreferDirectDeviceWrite(64 * GiB), - "UMA: single all-flags heap maps directly"); - } - - // --- non-ReBAR discrete: small 256 MiB BAR window (heap 1) separate from - // an 8 GiB VRAM heap (heap 0) -> stage large, map small ------------ - { - SetMemory( - { {DEVICE_LOCAL, 0}, // pure VRAM - {HOST_VISIBLE | HOST_COHERENT, 2}, // system RAM - {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} }, // BAR window - { {8 * GiB, HEAP_DEVICE_LOCAL}, // heap 0: VRAM - {256 * MiB, HEAP_DEVICE_LOCAL}, // heap 1: BAR - {32 * GiB, 0} }); // heap 2: RAM - Check(!Device::PreferDirectDeviceWrite(128 * MiB), - "small BAR: a large buffer stages (would exhaust the 256 MiB window)"); - Check(Device::PreferDirectDeviceWrite(8 * MiB), - "small BAR: a small/hot buffer maps directly into the window"); - // Budget is 1/8 of the window = 32 MiB. Boundary checks. - Check(Device::PreferDirectDeviceWrite(32 * MiB), - "small BAR: a buffer exactly at the per-buffer budget still maps"); - Check(!Device::PreferDirectDeviceWrite(32 * MiB + 1), - "small BAR: one byte over the budget stages"); - } - - // --- no host-visible device-local type (no resizable BAR) -> must stage - - { - SetMemory( - { {DEVICE_LOCAL, 0}, - {HOST_VISIBLE | HOST_COHERENT, 1}, - {HOST_VISIBLE | HOST_COHERENT | HOST_CACHED, 1} }, - { {8 * GiB, HEAP_DEVICE_LOCAL}, {32 * GiB, 0} }); - Check(!Device::PreferDirectDeviceWrite(8), - "no DEVICE_LOCAL|HOST_VISIBLE type -> staging is mandatory even for a tiny buffer"); - } - - // --- 90% threshold: a BAR heap that is most-but-not-all of VRAM counts as - // ReBAR; one well below the threshold is treated as a window -------- - { - // BAR heap = 9.5 GiB, VRAM = 10 GiB -> 95% >= 90% -> ReBAR -> direct. - SetMemory( - { {DEVICE_LOCAL, 0}, - {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} }, - { {10 * GiB, HEAP_DEVICE_LOCAL}, {VkDeviceSize(9.5 * GiB), HEAP_DEVICE_LOCAL} }); - Check(Device::PreferDirectDeviceWrite(4 * GiB), - "BAR >= 90% of VRAM is treated as ReBAR: large buffer still maps"); - - // BAR heap = 2 GiB, VRAM = 10 GiB -> 20% < 90% -> window -> stage big. - SetMemory( - { {DEVICE_LOCAL, 0}, - {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} }, - { {10 * GiB, HEAP_DEVICE_LOCAL}, {2 * GiB, HEAP_DEVICE_LOCAL} }); - Check(!Device::PreferDirectDeviceWrite(4 * GiB), - "BAR well below 90% of VRAM is a window: large buffer stages"); - Check(Device::PreferDirectDeviceWrite(128 * MiB), - "BAR window of 2 GiB still maps a small buffer (budget = 256 MiB)"); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/VulkanBufferFlushGate/main.cpp b/tests/VulkanBufferFlushGate/main.cpp deleted file mode 100644 index 2da4620..0000000 --- a/tests/VulkanBufferFlushGate/main.cpp +++ /dev/null @@ -1,73 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #60: VulkanBuffer::FlushDevice / FlushHost used to -// call vkFlushMappedMemoryRanges / vkInvalidateMappedMemoryRanges -// unconditionally. They now record the *chosen* memory type's propertyFlags at -// Create time and skip the flush/invalidate when the memory is HOST_COHERENT -// (coherent memory makes host/device writes mutually visible with no explicit -// flush). -// -// The gate is pure logic over the recorded flags, so this test drives it -// directly with no GPU device: it stamps memoryPropertyFlagsChosen and verifies -// the coherent path returns without issuing any Vulkan call. With no device -// created, Device::device is VK_NULL_HANDLE — so if the gate were removed, the -// flush call would dereference a null dispatch handle and crash. Reaching the -// line after the calls is the assertion that the gate held. - -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; -constexpr auto HOST_VISIBLE = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; -constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - -} // namespace - -int main() { - // No device is created, so Device::device stays VK_NULL_HANDLE; any real - // flush/invalidate call below would fault. The coherent gate must prevent - // it. - Check(Device::device == VK_NULL_HANDLE, - "no Vulkan device created — a real flush would fault"); - - { - VulkanBuffer buf; - // Coherent type chosen (even though no COHERENT bit was requested): - // both flush paths must early-return without touching Vulkan. - buf.memoryPropertyFlagsChosen = HOST_VISIBLE | HOST_COHERENT; - buf.FlushDevice(); - buf.FlushHost(); - Check(true, "FlushDevice/FlushHost skip the Vulkan call on coherent memory"); - } - - { - // A non-coherent type would require the flush — verify the recorded - // flags reflect the gate's decision input rather than the request. The - // bit test is the exact condition FlushDevice/FlushHost branch on. - VulkanBuffer buf; - buf.memoryPropertyFlagsChosen = HOST_VISIBLE | DEVICE_LOCAL; - Check(!(buf.memoryPropertyFlagsChosen & HOST_COHERENT), - "non-coherent chosen type is not gated as coherent (flush still required)"); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/VulkanBufferRangedFlush/main.cpp b/tests/VulkanBufferRangedFlush/main.cpp deleted file mode 100644 index 948a4b4..0000000 --- a/tests/VulkanBufferRangedFlush/main.cpp +++ /dev/null @@ -1,108 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for the ranged FlushDevice (UI descriptor registration): -// WriteBufferDescriptor / WriteSampledImageDescriptor / RegisterSampler used to -// FlushDevice() the whole multi-KB descriptor heap after writing one few-byte -// descriptor. They now flush only the written byte range via -// VulkanBuffer::FlushDevice(offset, bytes), which rounds the range outward to -// VkPhysicalDeviceLimits::nonCoherentAtomSize (vkFlushMappedMemoryRanges rejects -// an unaligned sub-buffer offset/size). -// -// The rounding lives in AlignMappedFlushRange — pure math with no Vulkan -// dependency — so this drives it directly with no GPU device, and also confirms -// the ranged FlushDevice keeps the issue-#60 coherent-memory gate (a real flush -// would fault with Device::device == VK_NULL_HANDLE). - -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -constexpr auto HOST_VISIBLE = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; -constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; -constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; - -} // namespace - -int main() { - Check(Device::device == VK_NULL_HANDLE, - "no Vulkan device created — a real flush would fault"); - - // ── AlignMappedFlushRange: the spec demands offset and size be multiples - // of nonCoherentAtomSize (atom). Start rounds down, end rounds up. ── - { - // A descriptor wholly inside one atom expands to cover that atom. - auto r = AlignMappedFlushRange(/*offset*/ 80, /*bytes*/ 16, - /*atom*/ 64, /*mappingSize*/ 4096); - Check(r.offset == 64 && r.size == 64, - "sub-atom range rounds out to the enclosing atom [64,128)"); - Check(r.offset % 64 == 0 && r.size % 64 == 0, - "rounded offset and size are atom multiples"); - } - { - // A range straddling an atom boundary expands both ways. - auto r = AlignMappedFlushRange(/*offset*/ 60, /*bytes*/ 8, - /*atom*/ 64, /*mappingSize*/ 4096); - Check(r.offset == 0 && r.size == 128, - "straddling range rounds out to [0,128)"); - } - { - // Already-aligned range is left exactly as is. - auto r = AlignMappedFlushRange(/*offset*/ 128, /*bytes*/ 64, - /*atom*/ 64, /*mappingSize*/ 4096); - Check(r.offset == 128 && r.size == 64, - "already atom-aligned range is unchanged"); - } - { - // atom == 1 (no coherence granularity): range passes through verbatim. - auto r = AlignMappedFlushRange(/*offset*/ 80, /*bytes*/ 16, - /*atom*/ 1, /*mappingSize*/ 4096); - Check(r.offset == 80 && r.size == 16, - "atom==1 leaves the range exact"); - } - { - // Near the end of a non-atom-aligned mapping: the rounded-up end is - // clamped to mappingSize so offset+size == mapping size (the spec's - // exception that permits a non-atom-multiple size). - auto r = AlignMappedFlushRange(/*offset*/ 4030, /*bytes*/ 8, - /*atom*/ 64, /*mappingSize*/ 4040); - Check(r.offset == 3968 && r.offset + r.size == 4040, - "tail range clamps its end to the mapping size"); - Check(r.offset % 64 == 0, - "clamped range still has an atom-aligned offset"); - } - - // ── Ranged FlushDevice keeps the coherent-memory gate (issue #60). ── - { - VulkanBuffer buf; - buf.memoryPropertyFlagsChosen = HOST_VISIBLE | HOST_COHERENT; - buf.mappedSize = 4096; - buf.FlushDevice(/*offset*/ 80, /*bytes*/ 16); // must early-return - Check(true, "ranged FlushDevice skips the Vulkan call on coherent memory"); - } - { - VulkanBuffer buf; - buf.memoryPropertyFlagsChosen = HOST_VISIBLE | DEVICE_LOCAL; - Check(!(buf.memoryPropertyFlagsChosen & HOST_COHERENT), - "non-coherent chosen type is not gated as coherent (ranged flush still required)"); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -} diff --git a/tests/VulkanBufferResizeReuse/main.cpp b/tests/VulkanBufferResizeReuse/main.cpp deleted file mode 100644 index f098138..0000000 --- a/tests/VulkanBufferResizeReuse/main.cpp +++ /dev/null @@ -1,118 +0,0 @@ -//SPDX-License-Identifier: LGPL-3.0-only -//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® - -// Regression test for issue #63: VulkanBuffer::Resize used to unconditionally -// destroy + reallocate. It now reuses the existing allocation in place when the -// new request still fits within the created capacity AND the immutable-at-create -// properties match (usage flags are fixed at create; the chosen memory type must -// still satisfy the required property flags). The reuse path only shrinks `size` -// and issues no Vulkan call. -// -// The guard is pure logic over recorded fields, so this test drives it directly -// with no GPU device: it stamps a fake non-null buffer handle plus capacity / -// usage / chosen-flags, then calls Resize. With Device::device == VK_NULL_HANDLE -// any real destroy/reallocate would dereference a null dispatch handle and -// crash — so reaching the line after a reuse Resize is the assertion that the -// in-place path was taken. The realloc path (flag/size mismatch) is intentionally -// not exercised here because it would issue real Vulkan calls. - -#include -#include -#include "vulkan/vulkan.h" - -import Crafter.Graphics; -import std; -using namespace Crafter; - -namespace { - -int failures = 0; - -void Check(bool ok, std::string_view what) { - std::println("{} {}", ok ? "PASS" : "FAIL", what); - if (!ok) ++failures; -} - -constexpr auto DEVICE_LOCAL = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; -constexpr auto HOST_VISIBLE = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT; -constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; - -constexpr VkBufferUsageFlags2 USAGE = - VK_BUFFER_USAGE_2_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT; - -// A fake non-null, non-dereferenced handle: Resize's reuse path never touches it. -VkBuffer FakeHandle() { - return reinterpret_cast(static_cast(0x1)); -} - -// Stamp a buffer into the "already created with capacity C" state without -// touching the GPU, then neutralise it so the destructor's Clear() is skipped. -template -void NeutraliseHandle(Buf& buf) { - buf.buffer = VK_NULL_HANDLE; -} - -} // namespace - -int main() { - Check(Device::device == VK_NULL_HANDLE, - "no Vulkan device created — a real reallocate would fault"); - - { - // Shrink within capacity, flags match → reuse in place, only size shrinks. - VulkanBuffer buf; - buf.buffer = FakeHandle(); - buf.capacity = 16 * sizeof(float); - buf.size = 16 * sizeof(float); - buf.usageFlagsCreated = USAGE; - buf.memoryPropertyFlagsChosen = HOST_VISIBLE | HOST_COHERENT; - - buf.Resize(USAGE, HOST_VISIBLE, 4); // 4 floats <= 16-float capacity - Check(buf.buffer == FakeHandle(), - "fitting Resize reuses the existing handle (no realloc)"); - Check(buf.size == 4 * sizeof(float), - "reuse updates size to the new logical extent"); - Check(buf.capacity == 16 * sizeof(float), - "reuse leaves capacity at the original allocation size"); - NeutraliseHandle(buf); - } - - { - // Exact-fit request (size == capacity) is still a reuse. - VulkanBuffer buf; - buf.buffer = FakeHandle(); - buf.capacity = 8 * sizeof(float); - buf.size = 2 * sizeof(float); - buf.usageFlagsCreated = USAGE; - buf.memoryPropertyFlagsChosen = HOST_VISIBLE; - - buf.Resize(USAGE, HOST_VISIBLE, 8); - Check(buf.buffer == FakeHandle() && buf.size == 8 * sizeof(float), - "exact-capacity Resize reuses the allocation"); - NeutraliseHandle(buf); - } - - { - // Reuse requires the chosen memory type to still satisfy the required - // flags. Chosen type carries DEVICE_LOCAL, so a request for it is - // satisfied and reuse is allowed. - VulkanBuffer buf; - buf.buffer = FakeHandle(); - buf.capacity = 8 * sizeof(float); - buf.size = 8 * sizeof(float); - buf.usageFlagsCreated = USAGE; - buf.memoryPropertyFlagsChosen = HOST_VISIBLE | DEVICE_LOCAL; - - buf.Resize(USAGE, DEVICE_LOCAL, 4); - Check(buf.buffer == FakeHandle() && buf.size == 4 * sizeof(float), - "reuse allowed when chosen memory type satisfies required flags"); - NeutraliseHandle(buf); - } - - if (failures != 0) { - std::println("{} check(s) failed", failures); - return EXIT_FAILURE; - } - std::println("all checks passed"); - return EXIT_SUCCESS; -}