detached d40e1f9

#25 fix(drm): retry commits refused for memory instead of stopping the output

state
merged · fdd661a · merge commit
branch
fix/drm-enomem-retry → main
opened
bnema ·
updated

Problem

Under VRAM pressure amdgpu can refuse an atomic commit with ENOMEM (the kernel fails to pin the new framebuffer). The previous frame stays on screen, but NeferWL treated the error as fatal: the output stopped and stayed dark until the next hotplug.

Change

  • ENOMEM on a frame, state or modeset commit keeps the output: the last frame stays on screen and the commit retries with a backoff from 16 ms to 1 s, without limit and without the stuck-recovery modeset.
  • The frame lifecycle tracks the current wait kind (flip, busy, nomem) apart from the memory-refusal run, so EBUSY after ENOMEM keeps its event unblocking and modeset recovery.
  • Direct scanout refused for memory pauses scanout for 2 s and composes into the resident output images.
  • Signalled render fences are reaped during retries, so long refusals do not accumulate descriptors.
  • Protected (locked) commits keep their existing retry path: a refused frame is never certified or reported.
  • Logs: one warn per refusal run, another after 5 s, info commit recovered with attempts and outage.

Checks

make check, make race, make mocks-check, staticcheck ./...; unit tests for backoff, ENOMEM→EBUSY, lasting refusal, modeset retry, scanout fallback, protected path, fence reaping. Not tested on real hardware under memory pressure.

Out of scope: renderer out-of-memory and GBM allocation failures at start/resume still stop the output.

CommitSubjectAuthorDateSignatureStatus
d40e1f9fix(drm): reap signalled render fences during commit retriesbriceGPG Verified
7703bb9fix(drm): separate the frame wait reason from the memory-refusal runbriceGPG Verified
d707400fix(drm): retry commits refused for memory instead of stopping the outputbriceGPG Verified

Checks 1

35 files · +742 -1833
1/1
M .goreleaser.yaml +2 -2
@@ -60,7 +60,7 @@ nfpms:
6060 homepage: https://git.bnema.dev/neferwl
6161 maintainer: "bnema <b@bnema.dev>"
6262 description: A Wayland compositor that spends its frames on your apps, not on itself
63- license: AGPL-3.0-only
63+ license: GPL-3.0-only
6464 section: x11
6565 bindir: /usr/bin
6666 # Loaded at runtime (purego, no cgo): dpkg-shlibdeps cannot see them.
@@ -192,7 +192,7 @@ aurs:
192192 description: A Wayland compositor that spends its frames on your apps, not on itself
193193 maintainers:
194194 - "bnema <b at bnema dot dev>"
195- license: AGPL-3.0-only
195+ license: GPL-3.0-only
196196 # The release workflow verifies and publishes the generated PKGBUILD.
197197 skip_upload: true
198198 # publish.sh ships the file next to the PKGBUILD.
@@ -60,7 +60,7 @@ nfpms:
60 homepage: https://git.bnema.dev/neferwl60 homepage: https://git.bnema.dev/neferwl
61 maintainer: "bnema <b@bnema.dev>"61 maintainer: "bnema <b@bnema.dev>"
62 description: A Wayland compositor that spends its frames on your apps, not on itself62 description: A Wayland compositor that spends its frames on your apps, not on itself
63- license: AGPL-3.0-only63+ license: GPL-3.0-only
64 section: x1164 section: x11
65 bindir: /usr/bin65 bindir: /usr/bin
66 # Loaded at runtime (purego, no cgo): dpkg-shlibdeps cannot see them.66 # Loaded at runtime (purego, no cgo): dpkg-shlibdeps cannot see them.
@@ -192,7 +192,7 @@ aurs:
192 description: A Wayland compositor that spends its frames on your apps, not on itself192 description: A Wayland compositor that spends its frames on your apps, not on itself
193 maintainers:193 maintainers:
194 - "bnema <b at bnema dot dev>"194 - "bnema <b at bnema dot dev>"
195- license: AGPL-3.0-only195+ license: GPL-3.0-only
196 # The release workflow verifies and publishes the generated PKGBUILD.196 # The release workflow verifies and publishes the generated PKGBUILD.
197 skip_upload: true197 skip_upload: true
198 # publish.sh ships the file next to the PKGBUILD.198 # publish.sh ships the file next to the PKGBUILD.
M .mockery.yml +0 -5
@@ -44,11 +44,6 @@ packages:
4444 dir: internal/adapters/drm
4545 pkgname: drm
4646 filename: image_buffer_mock_test.go
47- cursorImages:
48- config:
49- dir: internal/adapters/drm
50- pkgname: drm
51- filename: cursor_images_mock_test.go
5247 github.com/bnema/neferwl/internal/adapters/seat:
5348 interfaces:
5449 disabler:
@@ -44,11 +44,6 @@ packages:
44 dir: internal/adapters/drm44 dir: internal/adapters/drm
45 pkgname: drm45 pkgname: drm
46 filename: image_buffer_mock_test.go46 filename: image_buffer_mock_test.go
47- cursorImages:
48- config:
49- dir: internal/adapters/drm
50- pkgname: drm
51- filename: cursor_images_mock_test.go
52 github.com/bnema/neferwl/internal/adapters/seat:47 github.com/bnema/neferwl/internal/adapters/seat:
53 interfaces:48 interfaces:
54 disabler:49 disabler:
M LICENSE +80 -67
@@ -1,5 +1,5 @@
1- GNU AFFERO GENERAL PUBLIC LICENSE
2- Version 3, 19 November 2007
1+ GNU GENERAL PUBLIC LICENSE
2+ Version 3, 29 June 2007
33
44 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
55 Everyone is permitted to copy and distribute verbatim copies
@@ -7,15 +7,17 @@
77
88 Preamble
99
10- The GNU Affero General Public License is a free, copyleft license for
11-software and other kinds of works, specifically designed to ensure
12-cooperation with the community in the case of network server software.
10+ The GNU General Public License is a free, copyleft license for
11+software and other kinds of works.
1312
1413 The licenses for most software and other practical works are designed
1514 to take away your freedom to share and change the works. By contrast,
16-our General Public Licenses are intended to guarantee your freedom to
15+the GNU General Public License is intended to guarantee your freedom to
1716 share and change all versions of a program--to make sure it remains free
18-software for all its users.
17+software for all its users. We, the Free Software Foundation, use the
18+GNU General Public License for most of our software; it applies also to
19+any other work released this way by its authors. You can apply it to
20+your programs, too.
1921
2022 When we speak of free software, we are referring to freedom, not
2123 price. Our General Public Licenses are designed to make sure that you
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
2426 want it, that you can change the software or use pieces of it in new
2527 free programs, and that you know you can do these things.
2628
27- Developers that use our General Public Licenses protect your rights
28-with two steps: (1) assert copyright on the software, and (2) offer
29-you this License which gives you legal permission to copy, distribute
30-and/or modify the software.
31-
32- A secondary benefit of defending all users' freedom is that
33-improvements made in alternate versions of the program, if they
34-receive widespread use, become available for other developers to
35-incorporate. Many developers of free software are heartened and
36-encouraged by the resulting cooperation. However, in the case of
37-software used on network servers, this result may fail to come about.
38-The GNU General Public License permits making a modified version and
39-letting the public access it on a server without ever releasing its
40-source code to the public.
41-
42- The GNU Affero General Public License is designed specifically to
43-ensure that, in such cases, the modified source code becomes available
44-to the community. It requires the operator of a network server to
45-provide the source code of the modified version running there to the
46-users of that server. Therefore, public use of a modified version, on
47-a publicly accessible server, gives the public access to the source
48-code of the modified version.
49-
50- An older license, called the Affero General Public License and
51-published by Affero, was designed to accomplish similar goals. This is
52-a different license, not a version of the Affero GPL, but Affero has
53-released a new version of the Affero GPL which permits relicensing under
54-this license.
29+ To protect your rights, we need to prevent others from denying you
30+these rights or asking you to surrender the rights. Therefore, you have
31+certain responsibilities if you distribute copies of the software, or if
32+you modify it: responsibilities to respect the freedom of others.
33+
34+ For example, if you distribute copies of such a program, whether
35+gratis or for a fee, you must pass on to the recipients the same
36+freedoms that you received. You must make sure that they, too, receive
37+or can get the source code. And you must show them these terms so they
38+know their rights.
39+
40+ Developers that use the GNU GPL protect your rights with two steps:
41+(1) assert copyright on the software, and (2) offer you this License
42+giving you legal permission to copy, distribute and/or modify it.
43+
44+ For the developers' and authors' protection, the GPL clearly explains
45+that there is no warranty for this free software. For both users' and
46+authors' sake, the GPL requires that modified versions be marked as
47+changed, so that their problems will not be attributed erroneously to
48+authors of previous versions.
49+
50+ Some devices are designed to deny users access to install or run
51+modified versions of the software inside them, although the manufacturer
52+can do so. This is fundamentally incompatible with the aim of
53+protecting users' freedom to change the software. The systematic
54+pattern of such abuse occurs in the area of products for individuals to
55+use, which is precisely where it is most unacceptable. Therefore, we
56+have designed this version of the GPL to prohibit the practice for those
57+products. If such problems arise substantially in other domains, we
58+stand ready to extend this provision to those domains in future versions
59+of the GPL, as needed to protect the freedom of users.
60+
61+ Finally, every program is threatened constantly by software patents.
62+States should not allow patents to restrict development and use of
63+software on general-purpose computers, but in those that do, we wish to
64+avoid the special danger that patents applied to a free program could
65+make it effectively proprietary. To prevent this, the GPL assures that
66+patents cannot be used to render the program non-free.
5567
5668 The precise terms and conditions for copying, distribution and
5769 modification follow.
@@ -60,7 +72,7 @@ modification follow.
6072
6173 0. Definitions.
6274
63- "This License" refers to version 3 of the GNU Affero General Public License.
75+ "This License" refers to version 3 of the GNU General Public License.
6476
6577 "Copyright" also means copyright-like laws that apply to other kinds of
6678 works, such as semiconductor masks.
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
537549 the Program, the only way you could satisfy both those terms and this
538550 License would be to refrain entirely from conveying the Program.
539551
540- 13. Remote Network Interaction; Use with the GNU General Public License.
541-
542- Notwithstanding any other provision of this License, if you modify the
543-Program, your modified version must prominently offer all users
544-interacting with it remotely through a computer network (if your version
545-supports such interaction) an opportunity to receive the Corresponding
546-Source of your version by providing access to the Corresponding Source
547-from a network server at no charge, through some standard or customary
548-means of facilitating copying of software. This Corresponding Source
549-shall include the Corresponding Source for any work covered by version 3
550-of the GNU General Public License that is incorporated pursuant to the
551-following paragraph.
552+ 13. Use with the GNU Affero General Public License.
552553
553554 Notwithstanding any other provision of this License, you have
554555 permission to link or combine any covered work with a work licensed
555-under version 3 of the GNU General Public License into a single
556+under version 3 of the GNU Affero General Public License into a single
556557 combined work, and to convey the resulting work. The terms of this
557558 License will continue to apply to the part which is the covered work,
558-but the work with which it is combined will remain governed by version
559-3 of the GNU General Public License.
559+but the special requirements of the GNU Affero General Public License,
560+section 13, concerning interaction through a network will apply to the
561+combination as such.
560562
561563 14. Revised Versions of this License.
562564
563565 The Free Software Foundation may publish revised and/or new versions of
564-the GNU Affero General Public License from time to time. Such new versions
565-will be similar in spirit to the present version, but may differ in detail to
566+the GNU General Public License from time to time. Such new versions will
567+be similar in spirit to the present version, but may differ in detail to
566568 address new problems or concerns.
567569
568570 Each version is given a distinguishing version number. If the
569-Program specifies that a certain numbered version of the GNU Affero General
571+Program specifies that a certain numbered version of the GNU General
570572 Public License "or any later version" applies to it, you have the
571573 option of following the terms and conditions either of that numbered
572574 version or of any later version published by the Free Software
573575 Foundation. If the Program does not specify a version number of the
574-GNU Affero General Public License, you may choose any version ever published
576+GNU General Public License, you may choose any version ever published
575577 by the Free Software Foundation.
576578
577579 If the Program specifies that a proxy can decide which future
578-versions of the GNU Affero General Public License can be used, that proxy's
580+versions of the GNU General Public License can be used, that proxy's
579581 public statement of acceptance of a version permanently authorizes you
580582 to choose that version for the Program.
581583
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
633635 Copyright (C) <year> <name of author>
634636
635637 This program is free software: you can redistribute it and/or modify
636- it under the terms of the GNU Affero General Public License as published by
638+ it under the terms of the GNU General Public License as published by
637639 the Free Software Foundation, either version 3 of the License, or
638640 (at your option) any later version.
639641
640642 This program is distributed in the hope that it will be useful,
641643 but WITHOUT ANY WARRANTY; without even the implied warranty of
642644 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643- GNU Affero General Public License for more details.
645+ GNU General Public License for more details.
644646
645- You should have received a copy of the GNU Affero General Public License
647+ You should have received a copy of the GNU General Public License
646648 along with this program. If not, see <https://www.gnu.org/licenses/>.
647649
648650 Also add information on how to contact you by electronic and paper mail.
649651
650- If your software can interact with users remotely through a computer
651-network, you should also make sure that it provides a way for users to
652-get its source. For example, if your program is a web application, its
653-interface could display a "Source" link that leads users to an archive
654-of the code. There are many ways you could offer source, and different
655-solutions will be better for different programs; see section 13 for the
656-specific requirements.
652+ If the program does terminal interaction, make it output a short
653+notice like this when it starts in an interactive mode:
654+
655+ <program> Copyright (C) <year> <name of author>
656+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657+ This is free software, and you are welcome to redistribute it
658+ under certain conditions; type `show c' for details.
659+
660+The hypothetical commands `show w' and `show c' should show the appropriate
661+parts of the General Public License. Of course, your program's commands
662+might be different; for a GUI interface, you would use an "about box".
657663
658664 You should also get your employer (if you work as a programmer) or school,
659665 if any, to sign a "copyright disclaimer" for the program, if necessary.
660-For more information on this, and how to apply and follow the GNU AGPL, see
666+For more information on this, and how to apply and follow the GNU GPL, see
661667 <https://www.gnu.org/licenses/>.
668+
669+ The GNU General Public License does not permit incorporating your program
670+into proprietary programs. If your program is a subroutine library, you
671+may consider it more useful to permit linking proprietary applications with
672+the library. If this is what you want to do, use the GNU Lesser General
673+Public License instead of this License. But first, please read
674+<https://www.gnu.org/licenses/why-not-lgpl.html>.
@@ -1,5 +1,5 @@
1- GNU AFFERO GENERAL PUBLIC LICENSE1+ GNU GENERAL PUBLIC LICENSE
2- Version 3, 19 November 20072+ Version 3, 29 June 2007
3 3
4 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>4 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5 Everyone is permitted to copy and distribute verbatim copies5 Everyone is permitted to copy and distribute verbatim copies
@@ -7,15 +7,17 @@
7 7
8 Preamble8 Preamble
9 9
10- The GNU Affero General Public License is a free, copyleft license for10+ The GNU General Public License is a free, copyleft license for
11-software and other kinds of works, specifically designed to ensure11+software and other kinds of works.
12-cooperation with the community in the case of network server software.
13 12
14 The licenses for most software and other practical works are designed13 The licenses for most software and other practical works are designed
15 to take away your freedom to share and change the works. By contrast,14 to take away your freedom to share and change the works. By contrast,
16-our General Public Licenses are intended to guarantee your freedom to15+the GNU General Public License is intended to guarantee your freedom to
17 share and change all versions of a program--to make sure it remains free16 share and change all versions of a program--to make sure it remains free
18-software for all its users.17+software for all its users. We, the Free Software Foundation, use the
18+GNU General Public License for most of our software; it applies also to
19+any other work released this way by its authors. You can apply it to
20+your programs, too.
19 21
20 When we speak of free software, we are referring to freedom, not22 When we speak of free software, we are referring to freedom, not
21 price. Our General Public Licenses are designed to make sure that you23 price. Our General Public Licenses are designed to make sure that you
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
24 want it, that you can change the software or use pieces of it in new26 want it, that you can change the software or use pieces of it in new
25 free programs, and that you know you can do these things.27 free programs, and that you know you can do these things.
26 28
27- Developers that use our General Public Licenses protect your rights29+ To protect your rights, we need to prevent others from denying you
28-with two steps: (1) assert copyright on the software, and (2) offer30+these rights or asking you to surrender the rights. Therefore, you have
29-you this License which gives you legal permission to copy, distribute31+certain responsibilities if you distribute copies of the software, or if
30-and/or modify the software.32+you modify it: responsibilities to respect the freedom of others.
31-33+
32- A secondary benefit of defending all users' freedom is that34+ For example, if you distribute copies of such a program, whether
33-improvements made in alternate versions of the program, if they35+gratis or for a fee, you must pass on to the recipients the same
34-receive widespread use, become available for other developers to36+freedoms that you received. You must make sure that they, too, receive
35-incorporate. Many developers of free software are heartened and37+or can get the source code. And you must show them these terms so they
36-encouraged by the resulting cooperation. However, in the case of38+know their rights.
37-software used on network servers, this result may fail to come about.39+
38-The GNU General Public License permits making a modified version and40+ Developers that use the GNU GPL protect your rights with two steps:
39-letting the public access it on a server without ever releasing its41+(1) assert copyright on the software, and (2) offer you this License
40-source code to the public.42+giving you legal permission to copy, distribute and/or modify it.
41-43+
42- The GNU Affero General Public License is designed specifically to44+ For the developers' and authors' protection, the GPL clearly explains
43-ensure that, in such cases, the modified source code becomes available45+that there is no warranty for this free software. For both users' and
44-to the community. It requires the operator of a network server to46+authors' sake, the GPL requires that modified versions be marked as
45-provide the source code of the modified version running there to the47+changed, so that their problems will not be attributed erroneously to
46-users of that server. Therefore, public use of a modified version, on48+authors of previous versions.
47-a publicly accessible server, gives the public access to the source49+
48-code of the modified version.50+ Some devices are designed to deny users access to install or run
49-51+modified versions of the software inside them, although the manufacturer
50- An older license, called the Affero General Public License and52+can do so. This is fundamentally incompatible with the aim of
51-published by Affero, was designed to accomplish similar goals. This is53+protecting users' freedom to change the software. The systematic
52-a different license, not a version of the Affero GPL, but Affero has54+pattern of such abuse occurs in the area of products for individuals to
53-released a new version of the Affero GPL which permits relicensing under55+use, which is precisely where it is most unacceptable. Therefore, we
54-this license.56+have designed this version of the GPL to prohibit the practice for those
57+products. If such problems arise substantially in other domains, we
58+stand ready to extend this provision to those domains in future versions
59+of the GPL, as needed to protect the freedom of users.
60+
61+ Finally, every program is threatened constantly by software patents.
62+States should not allow patents to restrict development and use of
63+software on general-purpose computers, but in those that do, we wish to
64+avoid the special danger that patents applied to a free program could
65+make it effectively proprietary. To prevent this, the GPL assures that
66+patents cannot be used to render the program non-free.
55 67
56 The precise terms and conditions for copying, distribution and68 The precise terms and conditions for copying, distribution and
57 modification follow.69 modification follow.
@@ -60,7 +72,7 @@ modification follow.
60 72
61 0. Definitions.73 0. Definitions.
62 74
63- "This License" refers to version 3 of the GNU Affero General Public License.75+ "This License" refers to version 3 of the GNU General Public License.
64 76
65 "Copyright" also means copyright-like laws that apply to other kinds of77 "Copyright" also means copyright-like laws that apply to other kinds of
66 works, such as semiconductor masks.78 works, such as semiconductor masks.
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
537 the Program, the only way you could satisfy both those terms and this549 the Program, the only way you could satisfy both those terms and this
538 License would be to refrain entirely from conveying the Program.550 License would be to refrain entirely from conveying the Program.
539 551
540- 13. Remote Network Interaction; Use with the GNU General Public License.552+ 13. Use with the GNU Affero General Public License.
541-
542- Notwithstanding any other provision of this License, if you modify the
543-Program, your modified version must prominently offer all users
544-interacting with it remotely through a computer network (if your version
545-supports such interaction) an opportunity to receive the Corresponding
546-Source of your version by providing access to the Corresponding Source
547-from a network server at no charge, through some standard or customary
548-means of facilitating copying of software. This Corresponding Source
549-shall include the Corresponding Source for any work covered by version 3
550-of the GNU General Public License that is incorporated pursuant to the
551-following paragraph.
552 553
553 Notwithstanding any other provision of this License, you have554 Notwithstanding any other provision of this License, you have
554 permission to link or combine any covered work with a work licensed555 permission to link or combine any covered work with a work licensed
555-under version 3 of the GNU General Public License into a single556+under version 3 of the GNU Affero General Public License into a single
556 combined work, and to convey the resulting work. The terms of this557 combined work, and to convey the resulting work. The terms of this
557 License will continue to apply to the part which is the covered work,558 License will continue to apply to the part which is the covered work,
558-but the work with which it is combined will remain governed by version559+but the special requirements of the GNU Affero General Public License,
559-3 of the GNU General Public License.560+section 13, concerning interaction through a network will apply to the
561+combination as such.
560 562
561 14. Revised Versions of this License.563 14. Revised Versions of this License.
562 564
563 The Free Software Foundation may publish revised and/or new versions of565 The Free Software Foundation may publish revised and/or new versions of
564-the GNU Affero General Public License from time to time. Such new versions566+the GNU General Public License from time to time. Such new versions will
565-will be similar in spirit to the present version, but may differ in detail to567+be similar in spirit to the present version, but may differ in detail to
566 address new problems or concerns.568 address new problems or concerns.
567 569
568 Each version is given a distinguishing version number. If the570 Each version is given a distinguishing version number. If the
569-Program specifies that a certain numbered version of the GNU Affero General571+Program specifies that a certain numbered version of the GNU General
570 Public License "or any later version" applies to it, you have the572 Public License "or any later version" applies to it, you have the
571 option of following the terms and conditions either of that numbered573 option of following the terms and conditions either of that numbered
572 version or of any later version published by the Free Software574 version or of any later version published by the Free Software
573 Foundation. If the Program does not specify a version number of the575 Foundation. If the Program does not specify a version number of the
574-GNU Affero General Public License, you may choose any version ever published576+GNU General Public License, you may choose any version ever published
575 by the Free Software Foundation.577 by the Free Software Foundation.
576 578
577 If the Program specifies that a proxy can decide which future579 If the Program specifies that a proxy can decide which future
578-versions of the GNU Affero General Public License can be used, that proxy's580+versions of the GNU General Public License can be used, that proxy's
579 public statement of acceptance of a version permanently authorizes you581 public statement of acceptance of a version permanently authorizes you
580 to choose that version for the Program.582 to choose that version for the Program.
581 583
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
633 Copyright (C) <year> <name of author>635 Copyright (C) <year> <name of author>
634 636
635 This program is free software: you can redistribute it and/or modify637 This program is free software: you can redistribute it and/or modify
636- it under the terms of the GNU Affero General Public License as published by638+ it under the terms of the GNU General Public License as published by
637 the Free Software Foundation, either version 3 of the License, or639 the Free Software Foundation, either version 3 of the License, or
638 (at your option) any later version.640 (at your option) any later version.
639 641
640 This program is distributed in the hope that it will be useful,642 This program is distributed in the hope that it will be useful,
641 but WITHOUT ANY WARRANTY; without even the implied warranty of643 but WITHOUT ANY WARRANTY; without even the implied warranty of
642 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the644 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643- GNU Affero General Public License for more details.645+ GNU General Public License for more details.
644 646
645- You should have received a copy of the GNU Affero General Public License647+ You should have received a copy of the GNU General Public License
646 along with this program. If not, see <https://www.gnu.org/licenses/>.648 along with this program. If not, see <https://www.gnu.org/licenses/>.
647 649
648 Also add information on how to contact you by electronic and paper mail.650 Also add information on how to contact you by electronic and paper mail.
649 651
650- If your software can interact with users remotely through a computer652+ If the program does terminal interaction, make it output a short
651-network, you should also make sure that it provides a way for users to653+notice like this when it starts in an interactive mode:
652-get its source. For example, if your program is a web application, its654+
653-interface could display a "Source" link that leads users to an archive655+ <program> Copyright (C) <year> <name of author>
654-of the code. There are many ways you could offer source, and different656+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
655-solutions will be better for different programs; see section 13 for the657+ This is free software, and you are welcome to redistribute it
656-specific requirements.658+ under certain conditions; type `show c' for details.
659+
660+The hypothetical commands `show w' and `show c' should show the appropriate
661+parts of the General Public License. Of course, your program's commands
662+might be different; for a GUI interface, you would use an "about box".
657 663
658 You should also get your employer (if you work as a programmer) or school,664 You should also get your employer (if you work as a programmer) or school,
659 if any, to sign a "copyright disclaimer" for the program, if necessary.665 if any, to sign a "copyright disclaimer" for the program, if necessary.
660-For more information on this, and how to apply and follow the GNU AGPL, see666+For more information on this, and how to apply and follow the GNU GPL, see
661 <https://www.gnu.org/licenses/>.667 <https://www.gnu.org/licenses/>.
668+
669+ The GNU General Public License does not permit incorporating your program
670+into proprietary programs. If your program is a subroutine library, you
671+may consider it more useful to permit linking proprietary applications with
672+the library. If this is what you want to do, use the GNU Lesser General
673+Public License instead of this License. But first, please read
674+<https://www.gnu.org/licenses/why-not-lgpl.html>.
M Makefile +1 -1
@@ -61,7 +61,7 @@ arch: adapter-imports
6161 # Adapters stay independent: production code may import only the shared
6262 # adapter libraries below. outputkit holds what both output backends (drm,
6363 # headless) run; the others are leaf helpers.
64-ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|hdrcursor|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb
64+ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb
6565 adapter-imports:
6666 @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \
6767 bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \
@@ -61,7 +61,7 @@ arch: adapter-imports
61 # Adapters stay independent: production code may import only the shared61 # Adapters stay independent: production code may import only the shared
62 # adapter libraries below. outputkit holds what both output backends (drm,62 # adapter libraries below. outputkit holds what both output backends (drm,
63 # headless) run; the others are leaf helpers.63 # headless) run; the others are leaf helpers.
64-ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|hdrcursor|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb64+ADAPTER_LIBS := fdwait|busretry|hintlabel|outputkit/capture|outputkit/corewake|outputkit/presented|outputkit/surfaces|captureallow|clock|logging|peerexe|sessionsecurity|syncfile|workspaceid|xkb
65 adapter-imports:65 adapter-imports:
66 @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \66 @edges=$$(CGO_ENABLED=0 go list -f '{{$$p := .ImportPath}}{{range .Imports}}{{$$p}} {{.}}{{"\n"}}{{end}}' ./internal/adapters/...) || exit 1; \
67 bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \67 bad=$$(echo "$$edges" | grep -E ' \S+/internal/adapters/' | grep -vE '/internal/adapters/($(ADAPTER_LIBS))$$' \
M README.md +2 -2
@@ -1,7 +1,7 @@
11 <h1 align="center">NeferWL</h1>
22
33 <p align="center">
4- <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square" alt="License: AGPLv3"></a>
4+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-GPLv3-blue?style=flat-square" alt="License: GPLv3"></a>
55 <a href="https://github.com/bnema/neferwl"><img src="https://img.shields.io/badge/platform-Linux-blue?style=flat-square" alt="Platform: Linux"></a>
66 <a href="https://github.com/bnema/neferwl/commits/main"><img src="https://img.shields.io/github/last-commit/bnema/neferwl/main?style=flat-square&logo=github" alt="Last commit"></a>
77 </p>
@@ -191,4 +191,4 @@ Before submitting, run `make check`, `make race`, `make mocks-check` and `static
191191
192192 ## License
193193
194-[GNU Affero General Public License v3.0](LICENSE).
194+[GNU General Public License v3.0](LICENSE).
@@ -1,7 +1,7 @@
1 <h1 align="center">NeferWL</h1>1 <h1 align="center">NeferWL</h1>
2 2
3 <p align="center">3 <p align="center">
4- <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPLv3-blue?style=flat-square" alt="License: AGPLv3"></a>4+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-GPLv3-blue?style=flat-square" alt="License: GPLv3"></a>
5 <a href="https://github.com/bnema/neferwl"><img src="https://img.shields.io/badge/platform-Linux-blue?style=flat-square" alt="Platform: Linux"></a>5 <a href="https://github.com/bnema/neferwl"><img src="https://img.shields.io/badge/platform-Linux-blue?style=flat-square" alt="Platform: Linux"></a>
6 <a href="https://github.com/bnema/neferwl/commits/main"><img src="https://img.shields.io/github/last-commit/bnema/neferwl/main?style=flat-square&logo=github" alt="Last commit"></a>6 <a href="https://github.com/bnema/neferwl/commits/main"><img src="https://img.shields.io/github/last-commit/bnema/neferwl/main?style=flat-square&logo=github" alt="Last commit"></a>
7 </p>7 </p>
@@ -191,4 +191,4 @@ Before submitting, run `make check`, `make race`, `make mocks-check` and `static
191 191
192 ## License192 ## License
193 193
194-[GNU Affero General Public License v3.0](LICENSE).194+[GNU General Public License v3.0](LICENSE).
M docs/dev/architecture.md +0 -7
@@ -32,13 +32,6 @@ A DRM output flips between two images the renderer draws into, with no CPU copy.
3232 - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23).
3333 - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable.
3434
35-### Cursor images
36-
37-The cursor plane shows the hardware cursor from two ARGB8888 images that the CPU writes only when the cursor image changes, never on a move.
38-
39-- **Allocation:** the images are KMS dumb buffers, mapped for the CPU, with the pitch the driver picks. The output converts them to HDR10 when it is in HDR.
40-- **Fallback:** when KMS cannot make dumb buffers, or the plane refuses them in a TEST_ONLY commit, the renderer's linear cursor images are used. When the plane refuses those too, the cursor is drawn into every frame (software cursor). The log line `hardware cursor` names the allocator in its `alloc` field (`dumb` or `vulkan`), and is a warning for `vulkan`.
41-
4235 I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them:
4336
4437 - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo.
@@ -32,13 +32,6 @@ A DRM output flips between two images the renderer draws into, with no CPU copy.
32 - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23).32 - **Fallback:** when libgbm is missing, GBM allocates nothing usable, or Vulkan or KMS refuses the images, the renderer allocates and exports them itself. Each kind (driver modifier, single-plane, linear) tries GBM, then the renderer. The log line `zero-copy output` names the allocator in its `alloc` field (`gbm` or `vulkan`), and is a warning for `vulkan`. This fallback is deprecated and goes away once GBM is validated on more setups (neferwl#23).
33 - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable.33 - **Sync:** nvidia-drm before 560.35.3 does not wait for `IN_FENCE_FD`. On it, the output waits for each frame's fence on the CPU and commits without one. A client buffer whose fence is still pending is composed, not scanned out, while `IN_FENCE_FD` is unavailable.
34 34
35-### Cursor images
36-
37-The cursor plane shows the hardware cursor from two ARGB8888 images that the CPU writes only when the cursor image changes, never on a move.
38-
39-- **Allocation:** the images are KMS dumb buffers, mapped for the CPU, with the pitch the driver picks. The output converts them to HDR10 when it is in HDR.
40-- **Fallback:** when KMS cannot make dumb buffers, or the plane refuses them in a TEST_ONLY commit, the renderer's linear cursor images are used. When the plane refuses those too, the cursor is drawn into every frame (software cursor). The log line `hardware cursor` names the allocator in its `alloc` field (`dumb` or `vulkan`), and is a warning for `vulkan`.
41-
42 I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them:35 I also like bringing more tools to the Go ecosystem. NeferWL grew its own libraries along the way, and other Go projects can use them:
43 36
44 - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo.37 - [purego-libwayland](https://github.com/bnema/purego-libwayland): libwayland-server without cgo.
M internal/adapters/drm/atomic.go +0 -9
@@ -89,15 +89,6 @@ type kms interface {
8989 // addFB imports a single-plane dmabuf as a framebuffer of format.
9090 addFB(b *ports.DMABuf, format uint32) (uint32, error)
9191 rmFB(id uint32) error
92- // createDumb allocates a width×height 32 bpp dumb buffer; mapDumb maps it
93- // for the CPU (read/write) and unmapDumb releases the mapping;
94- // addFBHandle makes a framebuffer of format from it; destroyDumb frees
95- // it. The framebuffer is removed with rmFB first.
96- createDumb(width, height int) (dumbBuffer, error)
97- mapDumb(b dumbBuffer) ([]byte, error)
98- unmapDumb(mem []byte) error
99- addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error)
100- destroyDumb(handle uint32) error
10192 createLease(objects []uint32) (*os.File, uint32, error)
10293 revokeLease(id uint32) error
10394 resources() ([]uint32, []uint32, error)
@@ -89,15 +89,6 @@ type kms interface {
89 // addFB imports a single-plane dmabuf as a framebuffer of format.89 // addFB imports a single-plane dmabuf as a framebuffer of format.
90 addFB(b *ports.DMABuf, format uint32) (uint32, error)90 addFB(b *ports.DMABuf, format uint32) (uint32, error)
91 rmFB(id uint32) error91 rmFB(id uint32) error
92- // createDumb allocates a width×height 32 bpp dumb buffer; mapDumb maps it
93- // for the CPU (read/write) and unmapDumb releases the mapping;
94- // addFBHandle makes a framebuffer of format from it; destroyDumb frees
95- // it. The framebuffer is removed with rmFB first.
96- createDumb(width, height int) (dumbBuffer, error)
97- mapDumb(b dumbBuffer) ([]byte, error)
98- unmapDumb(mem []byte) error
99- addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error)
100- destroyDumb(handle uint32) error
101 createLease(objects []uint32) (*os.File, uint32, error)92 createLease(objects []uint32) (*os.File, uint32, error)
102 revokeLease(id uint32) error93 revokeLease(id uint32) error
103 resources() ([]uint32, []uint32, error)94 resources() ([]uint32, []uint32, error)
M internal/adapters/drm/commit_test.go +6 -8
@@ -55,10 +55,9 @@ func TestBusyFrameCommitsAfterEvent(t *testing.T) {
5555 func TestCursorImageNeverOverwritesScreen(t *testing.T) {
5656 o, _, _ := testOutput(t)
5757 c := o.cursor
58- imgs := newMockcursorImages(t)
59- c.imgs = imgs
58+ r := portsmocks.NewMockRenderer(t)
6059 var written []int
61- imgs.EXPECT().write(mock.Anything, mock.Anything, 1, 1, float64(0)).RunAndReturn(func(i int, _ []byte, _, _ int, _ float64) error {
60+ r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
6261 written = append(written, i)
6362 return nil
6463 })
@@ -66,7 +65,7 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) {
6665 // Image 0 on screen: two changes before a commit both go to image 1.
6766 c.screen, c.cur, c.image = 90, 0, true
6867 for range 2 {
69- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil {
68+ if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
7069 t.Fatal(err)
7170 }
7271 }
@@ -75,12 +74,12 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) {
7574 }
7675 // Image 1 in flight, image 0 on screen: nothing is free.
7776 c.committed(cursorState{on: true, fb: 91})
78- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 || c.later == nil {
77+ if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
7978 t.Fatalf("wrote a busy image: %v %v", err, written)
8079 }
8180 // The flip frees image 0.
8281 c.landed()
83- if changed, err := c.flushLater(0); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
82+ if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
8483 t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
8584 }
8685 }
@@ -465,7 +464,7 @@ func TestCursorOffSkipsLoads(t *testing.T) {
465464 o, _, _ := testOutput(t)
466465 o.cursor.off = true
467466 loaded := false
468- o.setCursor(func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
467+ o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
469468 loaded = true
470469 return ports.CursorImage{}, nil
471470 }, ports.CursorChange{}, 1, 0)
@@ -498,7 +497,6 @@ func runReportsSeenAfterCursorCommit(t *testing.T) {
498497 r.EXPECT().UseTarget(mock.Anything).Return()
499498 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
500499 r.EXPECT().Close().Return().Once()
501- k.EXPECT().createDumb(64, 64).Return(dumbBuffer{}, unix.ENOSYS).Once() // renderer images
502500 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
503501 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
504502 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
@@ -55,10 +55,9 @@ func TestBusyFrameCommitsAfterEvent(t *testing.T) {
55 func TestCursorImageNeverOverwritesScreen(t *testing.T) {55 func TestCursorImageNeverOverwritesScreen(t *testing.T) {
56 o, _, _ := testOutput(t)56 o, _, _ := testOutput(t)
57 c := o.cursor57 c := o.cursor
58- imgs := newMockcursorImages(t)58+ r := portsmocks.NewMockRenderer(t)
59- c.imgs = imgs
60 var written []int59 var written []int
61- imgs.EXPECT().write(mock.Anything, mock.Anything, 1, 1, float64(0)).RunAndReturn(func(i int, _ []byte, _, _ int, _ float64) error {60+ r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
62 written = append(written, i)61 written = append(written, i)
63 return nil62 return nil
64 })63 })
@@ -66,7 +65,7 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) {
66 // Image 0 on screen: two changes before a commit both go to image 1.65 // Image 0 on screen: two changes before a commit both go to image 1.
67 c.screen, c.cur, c.image = 90, 0, true66 c.screen, c.cur, c.image = 90, 0, true
68 for range 2 {67 for range 2 {
69- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil {68+ if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
70 t.Fatal(err)69 t.Fatal(err)
71 }70 }
72 }71 }
@@ -75,12 +74,12 @@ func TestCursorImageNeverOverwritesScreen(t *testing.T) {
75 }74 }
76 // Image 1 in flight, image 0 on screen: nothing is free.75 // Image 1 in flight, image 0 on screen: nothing is free.
77 c.committed(cursorState{on: true, fb: 91})76 c.committed(cursorState{on: true, fb: 91})
78- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 || c.later == nil {77+ if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
79 t.Fatalf("wrote a busy image: %v %v", err, written)78 t.Fatalf("wrote a busy image: %v %v", err, written)
80 }79 }
81 // The flip frees image 0.80 // The flip frees image 0.
82 c.landed()81 c.landed()
83- if changed, err := c.flushLater(0); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {82+ if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
84 t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)83 t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
85 }84 }
86 }85 }
@@ -465,7 +464,7 @@ func TestCursorOffSkipsLoads(t *testing.T) {
465 o, _, _ := testOutput(t)464 o, _, _ := testOutput(t)
466 o.cursor.off = true465 o.cursor.off = true
467 loaded := false466 loaded := false
468- o.setCursor(func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {467+ o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
469 loaded = true468 loaded = true
470 return ports.CursorImage{}, nil469 return ports.CursorImage{}, nil
471 }, ports.CursorChange{}, 1, 0)470 }, ports.CursorChange{}, 1, 0)
@@ -498,7 +497,6 @@ func runReportsSeenAfterCursorCommit(t *testing.T) {
498 r.EXPECT().UseTarget(mock.Anything).Return()497 r.EXPECT().UseTarget(mock.Anything).Return()
499 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)498 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
500 r.EXPECT().Close().Return().Once()499 r.EXPECT().Close().Return().Once()
501- k.EXPECT().createDumb(64, 64).Return(dumbBuffer{}, unix.ENOSYS).Once() // renderer images
502 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()500 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
503 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()501 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
504 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()502 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
M internal/adapters/drm/cursor.go +29 -73
@@ -31,24 +31,14 @@ type Cursor struct {
3131 wake chan struct{}
3232
3333 // Owned by the output goroutine.
34- plane *plane
35- size int
36- // imgs are the two images (nil: none), alloc says who allocated them
37- // (allocDumb or allocVulkan) and fallback makes the renderer's images
38- // when KMS refuses the dumb ones. fbs mirrors their framebuffers; cur
39- // is on screen or next.
40- imgs cursorImages
41- alloc string
42- fallback func() (cursorImages, error)
43- fbs [2]uint32
34+ plane *plane
35+ size int
36+ fbs [2]uint32 // the two images; cur is on screen or next
4437 cur int
4538 image bool // an image is loaded
4639 hotX, hotY int
47- // last is a copy of the loaded image, written again into new images
48- // when the allocator changes.
49- last laterImage
50- applied cursorState // what the last commit put on the plane
51- commits int
40+ applied cursorState // what the last commit put on the plane
41+ commits int
5242 // screen is the image the plane scans out (0: none); flight is what
5343 // the pending commit puts on it when flying. An image is written only
5444 // when it is neither, so the plane never shows a half-written image.
@@ -121,70 +111,37 @@ func (c *Cursor) poke() {
121111 }
122112 }
123113
124-// setup allocates the two cursor images and their framebuffers: KMS dumb
125-// buffers first, then the renderer's images. dumbErr is why the dumb buffers
126-// were not used (nil when they are); err is set when no images exist.
127-func (c *Cursor) setup(k kms, r ports.Renderer) (dumbErr, err error) {
128- c.fallback = func() (cursorImages, error) { return newRendererCursor(k, r, c.size) }
129- d, dumbErr := newDumbCursor(k, c.size)
130- if dumbErr == nil {
131- c.install(d, allocDumb)
132- return nil, nil
133- }
134- imgs, err := c.fallback()
114+// setup allocates the two cursor images and their framebuffers.
115+func (c *Cursor) setup(k kms, r ports.Renderer) error {
116+ bufs, err := r.CursorBuffers(c.size)
135117 if err != nil {
136- return dumbErr, err
137- }
138- c.install(imgs, allocVulkan)
139- return dumbErr, nil
140-}
141-
142-// install makes imgs the cursor images; none is on screen or in flight.
143-func (c *Cursor) install(imgs cursorImages, alloc string) {
144- c.imgs, c.alloc = imgs, alloc
145- for i := range c.fbs {
146- c.fbs[i] = imgs.fb(i)
118+ return err
147119 }
148- c.cur = 0
149- c.screen, c.flight, c.flying, c.applied = 0, cursorState{}, false, cursorState{}
150-}
151-
152-// useFallback replaces refused dumb images with the renderer's and writes
153-// the current image into them. It reports false when it cannot: no fallback,
154-// already used, or no images either.
155-func (c *Cursor) useFallback(hdrNits float64) (bool, error) {
156- if c.alloc != allocDumb || c.fallback == nil {
157- return false, nil
120+ for i := range bufs {
121+ if err == nil {
122+ c.fbs[i], err = k.addFB(&bufs[i], fourccARGB)
123+ }
124+ for _, p := range bufs[i].Planes {
125+ p.File.Close()
126+ }
158127 }
159- imgs, err := c.fallback()
160128 if err != nil {
161- return false, err
162- }
163- c.imgs.close()
164- c.install(imgs, allocVulkan)
165- l := c.later
166- if l == nil && c.image {
167- l = &c.last
129+ c.free(k)
168130 }
169- c.image, c.later = false, nil
170- if l == nil {
171- return true, nil
172- }
173- return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits)
131+ return err
174132 }
175133
176134 // setImage loads premultiplied ARGB8888 pixels into an image neither on
177135 // screen nor in flight; when there is none it waits for the next commit
178136 // event (flushLater). An empty image hides the cursor (a client asked for
179137 // none).
180-// hdrNits is the SDR white of an HDR output (0: SDR).
181-func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64) error {
138+func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error {
182139 c.later = nil
183140 if w <= 0 || h <= 0 || len(pixels) < w*h*4 {
184141 c.image = false
185142 return nil
186143 }
187- if c.imgs == nil {
144+ if c.fbs[0] == 0 {
188145 return errors.New("no cursor images")
189146 }
190147 next := c.freeSlot()
@@ -192,11 +149,10 @@ func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64)
192149 c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY}
193150 return nil
194151 }
195- if err := c.imgs.write(next, pixels, w, h, hdrNits); err != nil {
152+ if err := r.WriteCursor(next, pixels, w, h); err != nil {
196153 return err
197154 }
198155 c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY
199- c.last = laterImage{pixels: append(c.last.pixels[:0], pixels[:w*h*4]...), w: w, h: h, hotX: hotX, hotY: hotY}
200156 return nil
201157 }
202158
@@ -213,12 +169,12 @@ func (c *Cursor) freeSlot() int {
213169
214170 // flushLater loads an image that waited for a free slot. It reports
215171 // whether the cursor changed.
216-func (c *Cursor) flushLater(hdrNits float64) (bool, error) {
172+func (c *Cursor) flushLater(r ports.Renderer) (bool, error) {
217173 l := c.later
218174 if l == nil || c.freeSlot() < 0 {
219175 return false, nil
220176 }
221- return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits)
177+ return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY)
222178 }
223179
224180 // committed records that a commit carrying state s is in flight.
@@ -273,13 +229,13 @@ func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) {
273229 req.set(p.id, p.prop("CRTC_H"), size)
274230 }
275231
276-// free removes the framebuffers and releases the images.
277-func (c *Cursor) free() {
278- if c.imgs != nil {
279- c.imgs.close()
280- c.imgs, c.alloc = nil, ""
232+func (c *Cursor) free(k kms) {
233+ for i, fb := range c.fbs {
234+ if fb != 0 {
235+ _ = k.rmFB(fb)
236+ c.fbs[i] = 0
237+ }
281238 }
282- c.fbs = [2]uint32{}
283239 c.image, c.later = false, nil
284240 c.screen, c.flying = 0, false
285241 }
@@ -31,24 +31,14 @@ type Cursor struct {
31 wake chan struct{}31 wake chan struct{}
32 32
33 // Owned by the output goroutine.33 // Owned by the output goroutine.
34- plane *plane34+ plane *plane
35- size int35+ size int
36- // imgs are the two images (nil: none), alloc says who allocated them36+ fbs [2]uint32 // the two images; cur is on screen or next
37- // (allocDumb or allocVulkan) and fallback makes the renderer's images
38- // when KMS refuses the dumb ones. fbs mirrors their framebuffers; cur
39- // is on screen or next.
40- imgs cursorImages
41- alloc string
42- fallback func() (cursorImages, error)
43- fbs [2]uint32
44 cur int37 cur int
45 image bool // an image is loaded38 image bool // an image is loaded
46 hotX, hotY int39 hotX, hotY int
47- // last is a copy of the loaded image, written again into new images40+ applied cursorState // what the last commit put on the plane
48- // when the allocator changes.41+ commits int
49- last laterImage
50- applied cursorState // what the last commit put on the plane
51- commits int
52 // screen is the image the plane scans out (0: none); flight is what42 // screen is the image the plane scans out (0: none); flight is what
53 // the pending commit puts on it when flying. An image is written only43 // the pending commit puts on it when flying. An image is written only
54 // when it is neither, so the plane never shows a half-written image.44 // when it is neither, so the plane never shows a half-written image.
@@ -121,70 +111,37 @@ func (c *Cursor) poke() {
121 }111 }
122 }112 }
123 113
124-// setup allocates the two cursor images and their framebuffers: KMS dumb114+// setup allocates the two cursor images and their framebuffers.
125-// buffers first, then the renderer's images. dumbErr is why the dumb buffers115+func (c *Cursor) setup(k kms, r ports.Renderer) error {
126-// were not used (nil when they are); err is set when no images exist.116+ bufs, err := r.CursorBuffers(c.size)
127-func (c *Cursor) setup(k kms, r ports.Renderer) (dumbErr, err error) {
128- c.fallback = func() (cursorImages, error) { return newRendererCursor(k, r, c.size) }
129- d, dumbErr := newDumbCursor(k, c.size)
130- if dumbErr == nil {
131- c.install(d, allocDumb)
132- return nil, nil
133- }
134- imgs, err := c.fallback()
135 if err != nil {117 if err != nil {
136- return dumbErr, err118+ return err
137- }
138- c.install(imgs, allocVulkan)
139- return dumbErr, nil
140-}
141-
142-// install makes imgs the cursor images; none is on screen or in flight.
143-func (c *Cursor) install(imgs cursorImages, alloc string) {
144- c.imgs, c.alloc = imgs, alloc
145- for i := range c.fbs {
146- c.fbs[i] = imgs.fb(i)
147 }119 }
148- c.cur = 0120+ for i := range bufs {
149- c.screen, c.flight, c.flying, c.applied = 0, cursorState{}, false, cursorState{}121+ if err == nil {
150-}122+ c.fbs[i], err = k.addFB(&bufs[i], fourccARGB)
151-123+ }
152-// useFallback replaces refused dumb images with the renderer's and writes124+ for _, p := range bufs[i].Planes {
153-// the current image into them. It reports false when it cannot: no fallback,125+ p.File.Close()
154-// already used, or no images either.126+ }
155-func (c *Cursor) useFallback(hdrNits float64) (bool, error) {
156- if c.alloc != allocDumb || c.fallback == nil {
157- return false, nil
158 }127 }
159- imgs, err := c.fallback()
160 if err != nil {128 if err != nil {
161- return false, err129+ c.free(k)
162- }
163- c.imgs.close()
164- c.install(imgs, allocVulkan)
165- l := c.later
166- if l == nil && c.image {
167- l = &c.last
168 }130 }
169- c.image, c.later = false, nil131+ return err
170- if l == nil {
171- return true, nil
172- }
173- return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits)
174 }132 }
175 133
176 // setImage loads premultiplied ARGB8888 pixels into an image neither on134 // setImage loads premultiplied ARGB8888 pixels into an image neither on
177 // screen nor in flight; when there is none it waits for the next commit135 // screen nor in flight; when there is none it waits for the next commit
178 // event (flushLater). An empty image hides the cursor (a client asked for136 // event (flushLater). An empty image hides the cursor (a client asked for
179 // none).137 // none).
180-// hdrNits is the SDR white of an HDR output (0: SDR).138+func (c *Cursor) setImage(r ports.Renderer, pixels []byte, w, h, hotX, hotY int) error {
181-func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64) error {
182 c.later = nil139 c.later = nil
183 if w <= 0 || h <= 0 || len(pixels) < w*h*4 {140 if w <= 0 || h <= 0 || len(pixels) < w*h*4 {
184 c.image = false141 c.image = false
185 return nil142 return nil
186 }143 }
187- if c.imgs == nil {144+ if c.fbs[0] == 0 {
188 return errors.New("no cursor images")145 return errors.New("no cursor images")
189 }146 }
190 next := c.freeSlot()147 next := c.freeSlot()
@@ -192,11 +149,10 @@ func (c *Cursor) setImage(pixels []byte, w, h, hotX, hotY int, hdrNits float64)
192 c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY}149 c.later = &laterImage{pixels: slices.Clone(pixels[:w*h*4]), w: w, h: h, hotX: hotX, hotY: hotY}
193 return nil150 return nil
194 }151 }
195- if err := c.imgs.write(next, pixels, w, h, hdrNits); err != nil {152+ if err := r.WriteCursor(next, pixels, w, h); err != nil {
196 return err153 return err
197 }154 }
198 c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY155 c.cur, c.image, c.hotX, c.hotY = next, true, hotX, hotY
199- c.last = laterImage{pixels: append(c.last.pixels[:0], pixels[:w*h*4]...), w: w, h: h, hotX: hotX, hotY: hotY}
200 return nil156 return nil
201 }157 }
202 158
@@ -213,12 +169,12 @@ func (c *Cursor) freeSlot() int {
213 169
214 // flushLater loads an image that waited for a free slot. It reports170 // flushLater loads an image that waited for a free slot. It reports
215 // whether the cursor changed.171 // whether the cursor changed.
216-func (c *Cursor) flushLater(hdrNits float64) (bool, error) {172+func (c *Cursor) flushLater(r ports.Renderer) (bool, error) {
217 l := c.later173 l := c.later
218 if l == nil || c.freeSlot() < 0 {174 if l == nil || c.freeSlot() < 0 {
219 return false, nil175 return false, nil
220 }176 }
221- return true, c.setImage(l.pixels, l.w, l.h, l.hotX, l.hotY, hdrNits)177+ return true, c.setImage(r, l.pixels, l.w, l.h, l.hotX, l.hotY)
222 }178 }
223 179
224 // committed records that a commit carrying state s is in flight.180 // committed records that a commit carrying state s is in flight.
@@ -273,13 +229,13 @@ func (c *Cursor) props(req *atomicReq, crtc uint32, s cursorState) {
273 req.set(p.id, p.prop("CRTC_H"), size)229 req.set(p.id, p.prop("CRTC_H"), size)
274 }230 }
275 231
276-// free removes the framebuffers and releases the images.232+func (c *Cursor) free(k kms) {
277-func (c *Cursor) free() {233+ for i, fb := range c.fbs {
278- if c.imgs != nil {234+ if fb != 0 {
279- c.imgs.close()235+ _ = k.rmFB(fb)
280- c.imgs, c.alloc = nil, ""236+ c.fbs[i] = 0
237+ }
281 }238 }
282- c.fbs = [2]uint32{}
283 c.image, c.later = false, nil239 c.image, c.later = false, nil
284 c.screen, c.flying = 0, false240 c.screen, c.flying = 0, false
285 }241 }
D internal/adapters/drm/cursor_images.go +0 -164
@@ -1,164 +0,0 @@
1-package drm
2-
3-import (
4- "errors"
5- "fmt"
6-
7- "github.com/bnema/neferwl/internal/adapters/hdrcursor"
8- "github.com/bnema/neferwl/internal/ports"
9-)
10-
11-// Who allocated the cursor images (the "alloc" log field).
12-//
13-// The renderer's cursor images (allocVulkan, shared with the output images)
14-// are the fallback.
15-const allocDumb = "dumb"
16-
17-// cursorImages are the two ARGB8888 images of the cursor plane and their
18-// framebuffers. The output goroutine owns them.
19-type cursorImages interface {
20- // fb is the framebuffer of image i.
21- fb(i int) uint32
22- // write fills image i with premultiplied ARGB8888 pixels (w×h, w*4 per
23- // row), cropped to the image and cleared around. hdrNits > 0 converts
24- // them to the HDR10 output encoding for that SDR white.
25- write(i int, pixels []byte, w, h int, hdrNits float64) error
26- // close removes the framebuffers and releases the images.
27- close()
28-}
29-
30-// dumbImage is one KMS dumb buffer mapped for the CPU.
31-type dumbImage struct {
32- handle, fb uint32
33- pitch int
34- mem []byte
35-}
36-
37-// dumbCursor are two KMS dumb buffers: KMS allocates what it scans out, the
38-// CPU writes the pixels. Renderer-allocated images are refused by some
39-// drivers (nvidia-drm) on the cursor plane.
40-type dumbCursor struct {
41- k kms
42- size int
43- img [2]dumbImage
44- cache hdrcursor.Cache
45-}
46-
47-func newDumbCursor(k kms, size int) (*dumbCursor, error) {
48- d := &dumbCursor{k: k, size: size}
49- for i := range d.img {
50- if err := d.alloc(&d.img[i]); err != nil {
51- d.close()
52- return nil, err
53- }
54- }
55- return d, nil
56-}
57-
58-func (d *dumbCursor) alloc(img *dumbImage) error {
59- b, err := d.k.createDumb(d.size, d.size)
60- if err != nil {
61- return fmt.Errorf("create dumb buffer: %w", err)
62- }
63- img.handle, img.pitch = b.handle, int(b.pitch)
64- if img.pitch < d.size*4 {
65- return fmt.Errorf("dumb buffer pitch %d, want at least %d", img.pitch, d.size*4)
66- }
67- if img.mem, err = d.k.mapDumb(b); err != nil {
68- return fmt.Errorf("map dumb buffer: %w", err)
69- }
70- if len(img.mem) < img.pitch*d.size {
71- return fmt.Errorf("dumb buffer mapping %d bytes, want %d", len(img.mem), img.pitch*d.size)
72- }
73- clear(img.mem)
74- if img.fb, err = d.k.addFBHandle(b, d.size, d.size, fourccARGB); err != nil {
75- return fmt.Errorf("dumb buffer framebuffer: %w", err)
76- }
77- return nil
78-}
79-
80-func (d *dumbCursor) fb(i int) uint32 { return d.img[i].fb }
81-
82-func (d *dumbCursor) write(i int, pixels []byte, w, h int, hdrNits float64) error {
83- if i < 0 || i >= len(d.img) || d.img[i].mem == nil {
84- return errors.New("no cursor image")
85- }
86- if w < 0 || h < 0 || len(pixels) < w*h*4 {
87- return errors.New("short cursor image")
88- }
89- img := &d.img[i]
90- mem := img.mem[:img.pitch*d.size]
91- clear(mem)
92- cw := min(w, d.size) * 4
93- src, stride := pixels, w*4
94- if hdrNits > 0 {
95- // Called only on image changes, never on moves.
96- src, stride = d.cache.Convert(pixels, w, h, d.size, hdrNits), cw
97- }
98- for y := range min(h, d.size) {
99- copy(mem[y*img.pitch:y*img.pitch+cw], src[y*stride:y*stride+cw])
100- }
101- return nil
102-}
103-
104-func (d *dumbCursor) close() {
105- for i := range d.img {
106- img := &d.img[i]
107- if img.fb != 0 {
108- _ = d.k.rmFB(img.fb)
109- }
110- if img.mem != nil {
111- _ = d.k.unmapDumb(img.mem)
112- }
113- if img.handle != 0 {
114- _ = d.k.destroyDumb(img.handle)
115- }
116- *img = dumbImage{}
117- }
118- d.cache.Reset()
119-}
120-
121-// rendererCursor are the renderer's cursor images (linear dmabufs the
122-// renderer allocates and writes), imported as framebuffers.
123-type rendererCursor struct {
124- k kms
125- r ports.Renderer
126- fbs [2]uint32
127-}
128-
129-func newRendererCursor(k kms, r ports.Renderer, size int) (*rendererCursor, error) {
130- bufs, err := r.CursorBuffers(size)
131- if err != nil {
132- return nil, err
133- }
134- c := &rendererCursor{k: k, r: r}
135- for i := range bufs {
136- if err == nil {
137- c.fbs[i], err = k.addFB(&bufs[i], fourccARGB)
138- }
139- for _, p := range bufs[i].Planes {
140- p.File.Close()
141- }
142- }
143- if err != nil {
144- c.close()
145- return nil, err
146- }
147- return c, nil
148-}
149-
150-func (c *rendererCursor) fb(i int) uint32 { return c.fbs[i] }
151-
152-// write relies on the renderer's own HDR state: hdrNits is not used.
153-func (c *rendererCursor) write(i int, pixels []byte, w, h int, _ float64) error {
154- return c.r.WriteCursor(i, pixels, w, h)
155-}
156-
157-func (c *rendererCursor) close() {
158- for i, fb := range c.fbs {
159- if fb != 0 {
160- _ = c.k.rmFB(fb)
161- c.fbs[i] = 0
162- }
163- }
164-}
@@ -1,164 +0,0 @@
1-package drm
2-
3-import (
4- "errors"
5- "fmt"
6-
7- "github.com/bnema/neferwl/internal/adapters/hdrcursor"
8- "github.com/bnema/neferwl/internal/ports"
9-)
10-
11-// Who allocated the cursor images (the "alloc" log field).
12-//
13-// The renderer's cursor images (allocVulkan, shared with the output images)
14-// are the fallback.
15-const allocDumb = "dumb"
16-
17-// cursorImages are the two ARGB8888 images of the cursor plane and their
18-// framebuffers. The output goroutine owns them.
19-type cursorImages interface {
20- // fb is the framebuffer of image i.
21- fb(i int) uint32
22- // write fills image i with premultiplied ARGB8888 pixels (w×h, w*4 per
23- // row), cropped to the image and cleared around. hdrNits > 0 converts
24- // them to the HDR10 output encoding for that SDR white.
25- write(i int, pixels []byte, w, h int, hdrNits float64) error
26- // close removes the framebuffers and releases the images.
27- close()
28-}
29-
30-// dumbImage is one KMS dumb buffer mapped for the CPU.
31-type dumbImage struct {
32- handle, fb uint32
33- pitch int
34- mem []byte
35-}
36-
37-// dumbCursor are two KMS dumb buffers: KMS allocates what it scans out, the
38-// CPU writes the pixels. Renderer-allocated images are refused by some
39-// drivers (nvidia-drm) on the cursor plane.
40-type dumbCursor struct {
41- k kms
42- size int
43- img [2]dumbImage
44- cache hdrcursor.Cache
45-}
46-
47-func newDumbCursor(k kms, size int) (*dumbCursor, error) {
48- d := &dumbCursor{k: k, size: size}
49- for i := range d.img {
50- if err := d.alloc(&d.img[i]); err != nil {
51- d.close()
52- return nil, err
53- }
54- }
55- return d, nil
56-}
57-
58-func (d *dumbCursor) alloc(img *dumbImage) error {
59- b, err := d.k.createDumb(d.size, d.size)
60- if err != nil {
61- return fmt.Errorf("create dumb buffer: %w", err)
62- }
63- img.handle, img.pitch = b.handle, int(b.pitch)
64- if img.pitch < d.size*4 {
65- return fmt.Errorf("dumb buffer pitch %d, want at least %d", img.pitch, d.size*4)
66- }
67- if img.mem, err = d.k.mapDumb(b); err != nil {
68- return fmt.Errorf("map dumb buffer: %w", err)
69- }
70- if len(img.mem) < img.pitch*d.size {
71- return fmt.Errorf("dumb buffer mapping %d bytes, want %d", len(img.mem), img.pitch*d.size)
72- }
73- clear(img.mem)
74- if img.fb, err = d.k.addFBHandle(b, d.size, d.size, fourccARGB); err != nil {
75- return fmt.Errorf("dumb buffer framebuffer: %w", err)
76- }
77- return nil
78-}
79-
80-func (d *dumbCursor) fb(i int) uint32 { return d.img[i].fb }
81-
82-func (d *dumbCursor) write(i int, pixels []byte, w, h int, hdrNits float64) error {
83- if i < 0 || i >= len(d.img) || d.img[i].mem == nil {
84- return errors.New("no cursor image")
85- }
86- if w < 0 || h < 0 || len(pixels) < w*h*4 {
87- return errors.New("short cursor image")
88- }
89- img := &d.img[i]
90- mem := img.mem[:img.pitch*d.size]
91- clear(mem)
92- cw := min(w, d.size) * 4
93- src, stride := pixels, w*4
94- if hdrNits > 0 {
95- // Called only on image changes, never on moves.
96- src, stride = d.cache.Convert(pixels, w, h, d.size, hdrNits), cw
97- }
98- for y := range min(h, d.size) {
99- copy(mem[y*img.pitch:y*img.pitch+cw], src[y*stride:y*stride+cw])
100- }
101- return nil
102-}
103-
104-func (d *dumbCursor) close() {
105- for i := range d.img {
106- img := &d.img[i]
107- if img.fb != 0 {
108- _ = d.k.rmFB(img.fb)
109- }
110- if img.mem != nil {
111- _ = d.k.unmapDumb(img.mem)
112- }
113- if img.handle != 0 {
114- _ = d.k.destroyDumb(img.handle)
115- }
116- *img = dumbImage{}
117- }
118- d.cache.Reset()
119-}
120-
121-// rendererCursor are the renderer's cursor images (linear dmabufs the
122-// renderer allocates and writes), imported as framebuffers.
123-type rendererCursor struct {
124- k kms
125- r ports.Renderer
126- fbs [2]uint32
127-}
128-
129-func newRendererCursor(k kms, r ports.Renderer, size int) (*rendererCursor, error) {
130- bufs, err := r.CursorBuffers(size)
131- if err != nil {
132- return nil, err
133- }
134- c := &rendererCursor{k: k, r: r}
135- for i := range bufs {
136- if err == nil {
137- c.fbs[i], err = k.addFB(&bufs[i], fourccARGB)
138- }
139- for _, p := range bufs[i].Planes {
140- p.File.Close()
141- }
142- }
143- if err != nil {
144- c.close()
145- return nil, err
146- }
147- return c, nil
148-}
149-
150-func (c *rendererCursor) fb(i int) uint32 { return c.fbs[i] }
151-
152-// write relies on the renderer's own HDR state: hdrNits is not used.
153-func (c *rendererCursor) write(i int, pixels []byte, w, h int, _ float64) error {
154- return c.r.WriteCursor(i, pixels, w, h)
155-}
156-
157-func (c *rendererCursor) close() {
158- for i, fb := range c.fbs {
159- if fb != 0 {
160- _ = c.k.rmFB(fb)
161- c.fbs[i] = 0
162- }
163- }
164-}
D internal/adapters/drm/cursor_images_mock_test.go +0 -204
@@ -1,204 +0,0 @@
1-// Code generated by mockery; DO NOT EDIT.
2-// github.com/vektra/mockery
3-// template: testify
4-
5-package drm
6-
7-import (
8- mock "github.com/stretchr/testify/mock"
9-)
10-
11-// newMockcursorImages creates a new instance of mockcursorImages. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12-// The first argument is typically a *testing.T value.
13-func newMockcursorImages(t interface {
14- mock.TestingT
15- Cleanup(func())
16-}) *mockcursorImages {
17- if helper, ok := t.(interface{ Helper() }); ok {
18- helper.Helper()
19- }
20-
21- mock := &mockcursorImages{}
22- mock.Mock.Test(t)
23-
24- t.Cleanup(func() {
25- if helper, ok := t.(interface{ Helper() }); ok {
26- helper.Helper()
27- }
28- mock.AssertExpectations(t)
29- })
30-
31- return mock
32-}
33-
34-// mockcursorImages is an autogenerated mock type for the cursorImages type
35-type mockcursorImages struct {
36- mock.Mock
37-}
38-
39-type mockcursorImages_Expecter struct {
40- mock *mock.Mock
41-}
42-
43-func (_m *mockcursorImages) EXPECT() *mockcursorImages_Expecter {
44- return &mockcursorImages_Expecter{mock: &_m.Mock}
45-}
46-
47-// close provides a mock function for the type mockcursorImages
48-func (_mock *mockcursorImages) close() {
49- _mock.Called()
50- return
51-}
52-
53-// mockcursorImages_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close'
54-type mockcursorImages_close_Call struct {
55- *mock.Call
56-}
57-
58-// close is a helper method to define mock.On call
59-func (_e *mockcursorImages_Expecter) close() *mockcursorImages_close_Call {
60- return &mockcursorImages_close_Call{Call: _e.mock.On("close")}
61-}
62-
63-func (_c *mockcursorImages_close_Call) Run(run func()) *mockcursorImages_close_Call {
64- _c.Call.Run(func(args mock.Arguments) {
65- run()
66- })
67- return _c
68-}
69-
70-func (_c *mockcursorImages_close_Call) Return() *mockcursorImages_close_Call {
71- _c.Call.Return()
72- return _c
73-}
74-
75-func (_c *mockcursorImages_close_Call) RunAndReturn(run func()) *mockcursorImages_close_Call {
76- _c.Run(run)
77- return _c
78-}
79-
80-// fb provides a mock function for the type mockcursorImages
81-func (_mock *mockcursorImages) fb(i int) uint32 {
82- ret := _mock.Called(i)
83-
84- if len(ret) == 0 {
85- panic("no return value specified for fb")
86- }
87-
88- var r0 uint32
89- if returnFunc, ok := ret.Get(0).(func(int) uint32); ok {
90- r0 = returnFunc(i)
91- } else {
92- r0 = ret.Get(0).(uint32)
93- }
94- return r0
95-}
96-
97-// mockcursorImages_fb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fb'
98-type mockcursorImages_fb_Call struct {
99- *mock.Call
100-}
101-
102-// fb is a helper method to define mock.On call
103-// - i int
104-func (_e *mockcursorImages_Expecter) fb(i any) *mockcursorImages_fb_Call {
105- return &mockcursorImages_fb_Call{Call: _e.mock.On("fb", i)}
106-}
107-
108-func (_c *mockcursorImages_fb_Call) Run(run func(i int)) *mockcursorImages_fb_Call {
109- _c.Call.Run(func(args mock.Arguments) {
110- var arg0 int
111- if args[0] != nil {
112- arg0 = args[0].(int)
113- }
114- run(
115- arg0,
116- )
117- })
118- return _c
119-}
120-
121-func (_c *mockcursorImages_fb_Call) Return(v uint32) *mockcursorImages_fb_Call {
122- _c.Call.Return(v)
123- return _c
124-}
125-
126-func (_c *mockcursorImages_fb_Call) RunAndReturn(run func(i int) uint32) *mockcursorImages_fb_Call {
127- _c.Call.Return(run)
128- return _c
129-}
130-
131-// write provides a mock function for the type mockcursorImages
132-func (_mock *mockcursorImages) write(i int, pixels []byte, w int, h int, hdrNits float64) error {
133- ret := _mock.Called(i, pixels, w, h, hdrNits)
134-
135- if len(ret) == 0 {
136- panic("no return value specified for write")
137- }
138-
139- var r0 error
140- if returnFunc, ok := ret.Get(0).(func(int, []byte, int, int, float64) error); ok {
141- r0 = returnFunc(i, pixels, w, h, hdrNits)
142- } else {
143- r0 = ret.Error(0)
144- }
145- return r0
146-}
147-
148-// mockcursorImages_write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'write'
149-type mockcursorImages_write_Call struct {
150- *mock.Call
151-}
152-
153-// write is a helper method to define mock.On call
154-// - i int
155-// - pixels []byte
156-// - w int
157-// - h int
158-// - hdrNits float64
159-func (_e *mockcursorImages_Expecter) write(i any, pixels any, w any, h any, hdrNits any) *mockcursorImages_write_Call {
160- return &mockcursorImages_write_Call{Call: _e.mock.On("write", i, pixels, w, h, hdrNits)}
161-}
162-
163-func (_c *mockcursorImages_write_Call) Run(run func(i int, pixels []byte, w int, h int, hdrNits float64)) *mockcursorImages_write_Call {
164- _c.Call.Run(func(args mock.Arguments) {
165- var arg0 int
166- if args[0] != nil {
167- arg0 = args[0].(int)
168- }
169- var arg1 []byte
170- if args[1] != nil {
171- arg1 = args[1].([]byte)
172- }
173- var arg2 int
174- if args[2] != nil {
175- arg2 = args[2].(int)
176- }
177- var arg3 int
178- if args[3] != nil {
179- arg3 = args[3].(int)
180- }
181- var arg4 float64
182- if args[4] != nil {
183- arg4 = args[4].(float64)
184- }
185- run(
186- arg0,
187- arg1,
188- arg2,
189- arg3,
190- arg4,
191- )
192- })
193- return _c
194-}
195-
196-func (_c *mockcursorImages_write_Call) Return(err error) *mockcursorImages_write_Call {
197- _c.Call.Return(err)
198- return _c
199-}
200-
201-func (_c *mockcursorImages_write_Call) RunAndReturn(run func(i int, pixels []byte, w int, h int, hdrNits float64) error) *mockcursorImages_write_Call {
202- _c.Call.Return(run)
203- return _c
204-}
@@ -1,204 +0,0 @@
1-// Code generated by mockery; DO NOT EDIT.
2-// github.com/vektra/mockery
3-// template: testify
4-
5-package drm
6-
7-import (
8- mock "github.com/stretchr/testify/mock"
9-)
10-
11-// newMockcursorImages creates a new instance of mockcursorImages. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12-// The first argument is typically a *testing.T value.
13-func newMockcursorImages(t interface {
14- mock.TestingT
15- Cleanup(func())
16-}) *mockcursorImages {
17- if helper, ok := t.(interface{ Helper() }); ok {
18- helper.Helper()
19- }
20-
21- mock := &mockcursorImages{}
22- mock.Mock.Test(t)
23-
24- t.Cleanup(func() {
25- if helper, ok := t.(interface{ Helper() }); ok {
26- helper.Helper()
27- }
28- mock.AssertExpectations(t)
29- })
30-
31- return mock
32-}
33-
34-// mockcursorImages is an autogenerated mock type for the cursorImages type
35-type mockcursorImages struct {
36- mock.Mock
37-}
38-
39-type mockcursorImages_Expecter struct {
40- mock *mock.Mock
41-}
42-
43-func (_m *mockcursorImages) EXPECT() *mockcursorImages_Expecter {
44- return &mockcursorImages_Expecter{mock: &_m.Mock}
45-}
46-
47-// close provides a mock function for the type mockcursorImages
48-func (_mock *mockcursorImages) close() {
49- _mock.Called()
50- return
51-}
52-
53-// mockcursorImages_close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'close'
54-type mockcursorImages_close_Call struct {
55- *mock.Call
56-}
57-
58-// close is a helper method to define mock.On call
59-func (_e *mockcursorImages_Expecter) close() *mockcursorImages_close_Call {
60- return &mockcursorImages_close_Call{Call: _e.mock.On("close")}
61-}
62-
63-func (_c *mockcursorImages_close_Call) Run(run func()) *mockcursorImages_close_Call {
64- _c.Call.Run(func(args mock.Arguments) {
65- run()
66- })
67- return _c
68-}
69-
70-func (_c *mockcursorImages_close_Call) Return() *mockcursorImages_close_Call {
71- _c.Call.Return()
72- return _c
73-}
74-
75-func (_c *mockcursorImages_close_Call) RunAndReturn(run func()) *mockcursorImages_close_Call {
76- _c.Run(run)
77- return _c
78-}
79-
80-// fb provides a mock function for the type mockcursorImages
81-func (_mock *mockcursorImages) fb(i int) uint32 {
82- ret := _mock.Called(i)
83-
84- if len(ret) == 0 {
85- panic("no return value specified for fb")
86- }
87-
88- var r0 uint32
89- if returnFunc, ok := ret.Get(0).(func(int) uint32); ok {
90- r0 = returnFunc(i)
91- } else {
92- r0 = ret.Get(0).(uint32)
93- }
94- return r0
95-}
96-
97-// mockcursorImages_fb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'fb'
98-type mockcursorImages_fb_Call struct {
99- *mock.Call
100-}
101-
102-// fb is a helper method to define mock.On call
103-// - i int
104-func (_e *mockcursorImages_Expecter) fb(i any) *mockcursorImages_fb_Call {
105- return &mockcursorImages_fb_Call{Call: _e.mock.On("fb", i)}
106-}
107-
108-func (_c *mockcursorImages_fb_Call) Run(run func(i int)) *mockcursorImages_fb_Call {
109- _c.Call.Run(func(args mock.Arguments) {
110- var arg0 int
111- if args[0] != nil {
112- arg0 = args[0].(int)
113- }
114- run(
115- arg0,
116- )
117- })
118- return _c
119-}
120-
121-func (_c *mockcursorImages_fb_Call) Return(v uint32) *mockcursorImages_fb_Call {
122- _c.Call.Return(v)
123- return _c
124-}
125-
126-func (_c *mockcursorImages_fb_Call) RunAndReturn(run func(i int) uint32) *mockcursorImages_fb_Call {
127- _c.Call.Return(run)
128- return _c
129-}
130-
131-// write provides a mock function for the type mockcursorImages
132-func (_mock *mockcursorImages) write(i int, pixels []byte, w int, h int, hdrNits float64) error {
133- ret := _mock.Called(i, pixels, w, h, hdrNits)
134-
135- if len(ret) == 0 {
136- panic("no return value specified for write")
137- }
138-
139- var r0 error
140- if returnFunc, ok := ret.Get(0).(func(int, []byte, int, int, float64) error); ok {
141- r0 = returnFunc(i, pixels, w, h, hdrNits)
142- } else {
143- r0 = ret.Error(0)
144- }
145- return r0
146-}
147-
148-// mockcursorImages_write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'write'
149-type mockcursorImages_write_Call struct {
150- *mock.Call
151-}
152-
153-// write is a helper method to define mock.On call
154-// - i int
155-// - pixels []byte
156-// - w int
157-// - h int
158-// - hdrNits float64
159-func (_e *mockcursorImages_Expecter) write(i any, pixels any, w any, h any, hdrNits any) *mockcursorImages_write_Call {
160- return &mockcursorImages_write_Call{Call: _e.mock.On("write", i, pixels, w, h, hdrNits)}
161-}
162-
163-func (_c *mockcursorImages_write_Call) Run(run func(i int, pixels []byte, w int, h int, hdrNits float64)) *mockcursorImages_write_Call {
164- _c.Call.Run(func(args mock.Arguments) {
165- var arg0 int
166- if args[0] != nil {
167- arg0 = args[0].(int)
168- }
169- var arg1 []byte
170- if args[1] != nil {
171- arg1 = args[1].([]byte)
172- }
173- var arg2 int
174- if args[2] != nil {
175- arg2 = args[2].(int)
176- }
177- var arg3 int
178- if args[3] != nil {
179- arg3 = args[3].(int)
180- }
181- var arg4 float64
182- if args[4] != nil {
183- arg4 = args[4].(float64)
184- }
185- run(
186- arg0,
187- arg1,
188- arg2,
189- arg3,
190- arg4,
191- )
192- })
193- return _c
194-}
195-
196-func (_c *mockcursorImages_write_Call) Return(err error) *mockcursorImages_write_Call {
197- _c.Call.Return(err)
198- return _c
199-}
200-
201-func (_c *mockcursorImages_write_Call) RunAndReturn(run func(i int, pixels []byte, w int, h int, hdrNits float64) error) *mockcursorImages_write_Call {
202- _c.Call.Return(run)
203- return _c
204-}
D internal/adapters/drm/cursor_images_test.go +0 -491
@@ -1,491 +0,0 @@
1-package drm
2-
3-import (
4- "bytes"
5- "errors"
6- "os"
7- "testing"
8-
9- portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10- "github.com/bnema/neferwl/internal/ports"
11- "github.com/stretchr/testify/mock"
12- "golang.org/x/sys/unix"
13-)
14-
15-const tSide = 16
16-
17-// dumbOK expects two dumb buffers with handles 10 and 11, mappings (with
18-// pitch bytes per row) returned in mem, and framebuffers 90 and 91.
19-func dumbOK(k *mockkms, pitch int) (mem [2][]byte) {
20- for i := range mem {
21- mem[i] = make([]byte, pitch*tSide)
22- h := uint32(10 + i)
23- b := dumbBuffer{handle: h, pitch: uint32(pitch), size: uint64(len(mem[i]))}
24- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
25- k.EXPECT().mapDumb(b).Return(mem[i], nil).Once()
26- k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90+uint32(i), nil).Once()
27- }
28- return mem
29-}
30-
31-func dumbFree(k *mockkms) {
32- for i := range 2 {
33- k.EXPECT().rmFB(90 + uint32(i)).Return(nil).Once()
34- k.EXPECT().destroyDumb(10 + uint32(i)).Return(nil).Once()
35- }
36- k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice()
37-}
38-
39-func rendererBufs(t *testing.T) [2]ports.DMABuf {
40- var b [2]ports.DMABuf
41- for i := range b {
42- f, w, err := os.Pipe()
43- if err != nil {
44- t.Fatal(err)
45- }
46- t.Cleanup(func() { _ = f.Close(); _ = w.Close() })
47- if err := w.Close(); err != nil {
48- t.Fatal(err)
49- }
50- b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
51- }
52- return b
53-}
54-
55-func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) {
56- b := rendererBufs(t)
57- r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once()
58- t.Cleanup(func() { assertPlanesClosed(t, b) })
59- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once()
60- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once()
61-}
62-
63-// Dumb buffers come first: the renderer allocates nothing.
64-func TestCursorSetupUsesDumbBuffers(t *testing.T) {
65- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
66- dumbOK(k, tSide*4)
67- c := newCursor(&plane{}, tSide)
68- dumbErr, err := c.setup(k, r)
69- if dumbErr != nil || err != nil {
70- t.Fatalf("setup: %v %v", dumbErr, err)
71- }
72- if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} {
73- t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs)
74- }
75- dumbFree(k)
76- c.free()
77- if c.imgs != nil || c.fbs != [2]uint32{} {
78- t.Fatal("images kept after free")
79- }
80-}
81-
82-// A dumb buffer that cannot be made (or mapped, or wrapped) falls back to
83-// the renderer's images, and what was made is released.
84-func TestCursorSetupFallsBackToRenderer(t *testing.T) {
85- for name, fail := range map[string]func(k *mockkms){
86- "create": func(k *mockkms) {
87- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
88- },
89- "map": func(k *mockkms) {
90- b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
91- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
92- k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once()
93- k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
94- },
95- "framebuffer": func(k *mockkms) {
96- b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
97- mem := make([]byte, b.size)
98- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
99- k.EXPECT().mapDumb(b).Return(mem, nil).Once()
100- k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once()
101- k.EXPECT().unmapDumb(mem).Return(nil).Once()
102- k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
103- },
104- "second buffer": func(k *mockkms) {
105- b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
106- mem := make([]byte, b.size)
107- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
108- k.EXPECT().mapDumb(b).Return(mem, nil).Once()
109- k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once()
110- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once()
111- k.EXPECT().rmFB(uint32(90)).Return(nil).Once()
112- k.EXPECT().unmapDumb(mem).Return(nil).Once()
113- k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
114- },
115- } {
116- t.Run(name, func(t *testing.T) {
117- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
118- fail(k)
119- rendererOK(t, k, r)
120- c := newCursor(&plane{}, tSide)
121- dumbErr, err := c.setup(k, r)
122- if dumbErr == nil || err != nil {
123- t.Fatalf("setup: %v %v", dumbErr, err)
124- }
125- if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} {
126- t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs)
127- }
128- })
129- }
130-}
131-
132-// Without either allocator there are no images.
133-func TestCursorSetupFailsWithoutAllocators(t *testing.T) {
134- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
135- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
136- r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once()
137- c := newCursor(&plane{}, tSide)
138- dumbErr, err := c.setup(k, r)
139- if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} {
140- t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs)
141- }
142- if _, err := c.useFallback(0); err != nil {
143- t.Fatal(err)
144- }
145-}
146-
147-// A renderer image whose framebuffer fails is released.
148-func TestRendererCursorReleasesOnFailure(t *testing.T) {
149- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
150- bufs := rendererBufs(t)
151- r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once()
152- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once()
153- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once()
154- k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
155- if _, err := newRendererCursor(k, r, tSide); err == nil {
156- t.Fatal("framebuffer failure not reported")
157- }
158- assertPlanesClosed(t, bufs)
159-}
160-
161-func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) {
162- t.Helper()
163- for i, b := range bufs {
164- for j, p := range b.Planes {
165- if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) {
166- t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err)
167- }
168- }
169- }
170-}
171-
172-// cursorOutput is a test output whose cursor plane has size tSide.
173-func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) {
174- o, k, commits := testOutput(t, errs...)
175- o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide)
176- return o, k, portsmocks.NewMockRenderer(t), commits
177-}
178-
179-// The plane refuses the dumb images: the renderer's are tried (TEST_ONLY),
180-// the image on the cursor is rewritten into them, and writes go there.
181-func TestCursorDumbRefusedUsesRendererImages(t *testing.T) {
182- o, k, r, commits := cursorOutput(t, unix.EINVAL)
183- dumbOK(k, tSide*4)
184- if _, err := o.cursor.setup(k, r); err != nil {
185- t.Fatal(err)
186- }
187- px := []byte{1, 2, 3, 4}
188- if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil {
189- t.Fatal(err)
190- }
191- dumbFree(k)
192- rendererOK(t, k, r)
193- var written []int
194- r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
195- written = append(written, i)
196- return nil
197- })
198- o.testCursor()
199- c := o.cursor
200- if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 {
201- t.Fatalf("off %v alloc %q fbs %v image %v hot %d,%d", c.off, c.alloc, c.fbs, c.image, c.hotX, c.hotY)
202- }
203- if len(written) != 1 || len(*commits) != 2 {
204- t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits))
205- }
206- if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly {
207- t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags)
208- }
209- // Further changes are written by the renderer, not into the freed buffers.
210- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 {
211- t.Fatalf("write after fallback: %v %v", err, written)
212- }
213-}
214-
215-// Both kinds refused: the software cursor takes over.
216-func TestCursorBothRefusedGoesOff(t *testing.T) {
217- o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL)
218- dumbOK(k, tSide*4)
219- if _, err := o.cursor.setup(k, r); err != nil {
220- t.Fatal(err)
221- }
222- dumbFree(k)
223- rendererOK(t, k, r)
224- if !o.testCursor() {
225- t.Fatal("no change reported")
226- }
227- if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 {
228- t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits))
229- }
230- if o.cursor.desired().on {
231- t.Fatal("cursor shown while off")
232- }
233- // The renderer's images are released with the output.
234- k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
235- k.EXPECT().rmFB(uint32(81)).Return(nil).Once()
236- o.cursor.free()
237-}
238-
239-// Dumb images the plane takes stay, with no second test.
240-func TestCursorDumbAcceptedStays(t *testing.T) {
241- for _, tc := range []struct {
242- name string
243- err error
244- }{{"accepted", nil}, {"non-refusal", unix.EIO}} {
245- t.Run(tc.name, func(t *testing.T) {
246- o, k, r, commits := cursorOutput(t, tc.err)
247- dumbOK(k, tSide*4)
248- if _, err := o.cursor.setup(k, r); err != nil {
249- t.Fatal(err)
250- }
251- if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 {
252- t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits))
253- }
254- dumbFree(k)
255- o.cursor.free()
256- })
257- }
258-}
259-
260-// Renderer images refused while no dumb ones exist: off, no retry.
261-func TestCursorRendererRefusedGoesOff(t *testing.T) {
262- o, k, r, commits := cursorOutput(t, unix.EINVAL)
263- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
264- rendererOK(t, k, r)
265- if _, err := o.cursor.setup(k, r); err != nil {
266- t.Fatal(err)
267- }
268- if !o.testCursor() || !o.cursor.off || len(*commits) != 1 {
269- t.Fatalf("off %v commits %d", o.cursor.off, len(*commits))
270- }
271-}
272-
273-// Writes go to the images that own the cursor, and the output's HDR white
274-// is passed along.
275-func TestCursorWritesGoToCurrentImages(t *testing.T) {
276- o, _, _, _ := cursorOutput(t)
277- imgs := newMockcursorImages(t)
278- imgs.EXPECT().fb(0).Return(90).Once()
279- imgs.EXPECT().fb(1).Return(91).Once()
280- o.cursor.install(imgs, allocDumb)
281- px := []byte{1, 2, 3, 4}
282- imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once()
283- if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil {
284- t.Fatal(err)
285- }
286- o.hdr.on, o.hdr.settings.SDRBrightness = true, 203
287- if got := o.cursorNits(); got != 203 {
288- t.Fatalf("HDR white %v", got)
289- }
290- o.cursor.screen = 90
291- imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once()
292- if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil {
293- t.Fatal("write error swallowed")
294- }
295-}
296-
297-// Dumb pitch may exceed the row size: rows land at the pitch, the image is
298-// cropped to the side and cleared around.
299-func TestDumbCursorWritePitchCropClear(t *testing.T) {
300- k := newMockkms(t)
301- const pitch = tSide*4 + 32
302- mem := dumbOK(k, pitch)
303- d, err := newDumbCursor(k, tSide)
304- if err != nil {
305- t.Fatal(err)
306- }
307- defer func() { dumbFree(k); d.close() }()
308- big := make([]byte, 40*40*4)
309- for y := range 40 {
310- for x := range 40 {
311- copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255})
312- }
313- }
314- if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide
315- t.Fatal(err)
316- }
317- last := (tSide - 1) * pitch
318- for _, y := range []int{0, 1, 7, tSide - 1} {
319- for _, x := range []int{0, 3, tSide - 1} {
320- got := mem[1][y*pitch+x*4 : y*pitch+x*4+4]
321- want := big[(y*40+x)*4 : (y*40+x)*4+4]
322- if !bytes.Equal(got, want) {
323- t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want)
324- }
325- }
326- }
327- for y := range tSide {
328- if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) {
329- t.Fatalf("row %d padding written", y)
330- }
331- }
332- if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil {
333- t.Fatal(err)
334- }
335- m := mem[1]
336- if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 {
337- t.Fatal("image not cleared around the new pixels")
338- }
339- // A wide, short image keeps its rows apart.
340- row := []byte{9, 9, 9, 9, 8, 8, 8, 8}
341- if err := d.write(0, append(row, row...), 2, 2, 0); err != nil {
342- t.Fatal(err)
343- }
344- if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 {
345- t.Fatal("rows of a narrow image")
346- }
347- if err := d.write(0, []byte{1}, 1, 1, 0); err == nil {
348- t.Fatal("short image accepted")
349- }
350- if err := d.write(2, nil, 0, 0, 0); err == nil {
351- t.Fatal("bad index accepted")
352- }
353-}
354-
355-// HDR cursors convert exactly as the renderer did (see hdrcursor's vector).
356-func TestDumbCursorWriteHDR(t *testing.T) {
357- k := newMockkms(t)
358- mem := dumbOK(k, tSide*4)
359- d, err := newDumbCursor(k, tSide)
360- if err != nil {
361- t.Fatal(err)
362- }
363- defer func() { dumbFree(k); d.close() }()
364- if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil {
365- t.Fatal(err)
366- }
367- if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 {
368- t.Fatalf("HDR white: %v", m[:4])
369- }
370- if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 {
371- t.Fatalf("SDR white: %v %v", err, mem[1][:4])
372- }
373- if d.cache.Len() != 4 {
374- t.Fatalf("cache %d", d.cache.Len())
375- }
376-}
377-
378-func TestCursorFallbackState(t *testing.T) {
379- for _, mode := range []string{"loaded", "queued", "hidden"} {
380- t.Run(mode, func(t *testing.T) {
381- o, k, r, commits := cursorOutput(t, unix.EINVAL)
382- dumbOK(k, tSide*4)
383- c := o.cursor
384- if _, err := c.setup(k, r); err != nil {
385- t.Fatal(err)
386- }
387- px := []byte{1, 2, 3, 4}
388- if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil {
389- t.Fatal(err)
390- }
391- c.screen = 90
392- c.committed(cursorState{on: true, fb: 91, x: 8, y: 9})
393- want := []byte{1, 2, 3, 4}
394- if mode == "queued" {
395- px = []byte{5, 6, 7, 8}
396- want = bytes.Clone(px)
397- if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil {
398- t.Fatal(err)
399- }
400- if c.later == nil {
401- t.Fatal("image not queued")
402- }
403- }
404- if mode == "hidden" {
405- if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil {
406- t.Fatal(err)
407- }
408- }
409- px[0] = 99 // Neither the loaded nor queued image may alias caller memory.
410- dumbFree(k)
411- rendererOK(t, k, r)
412- if mode != "hidden" {
413- r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once()
414- }
415- if !o.testCursor() {
416- t.Fatal("swap not reported")
417- }
418- if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil {
419- t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied)
420- }
421- c.landed()
422- if c.screen != 0 {
423- t.Fatal("old pending flight resurrected")
424- }
425- s := c.desired()
426- if mode == "hidden" {
427- if s.on || c.image {
428- t.Fatal("hidden cursor shown")
429- }
430- } else {
431- if !s.on || s.fb != 80 {
432- t.Fatalf("desired %+v", s)
433- }
434- if mode == "queued" && (c.hotX != 4 || c.hotY != 5) {
435- t.Fatal("queued hotspot lost")
436- }
437- }
438- req := &atomicReq{}
439- c.props(req, o.crtc, s)
440- if err := k.commit(req, 0, 0); err != nil {
441- t.Fatal(err)
442- }
443- wantFB := uint64(80)
444- if mode == "hidden" {
445- wantFB = 0
446- }
447- if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB {
448- t.Fatalf("next commit fb %d want %d", fb, wantFB)
449- }
450- c.committed(s)
451- c.landed()
452- if c.screen != uint32(wantFB) {
453- t.Fatalf("screen %d", c.screen)
454- }
455- })
456- }
457-}
458-
459-func TestCursorFallbackFailureGoesOff(t *testing.T) {
460- for _, mode := range []string{"allocation", "rewrite"} {
461- t.Run(mode, func(t *testing.T) {
462- o, k, r, commits := cursorOutput(t, unix.EINVAL)
463- dumbOK(k, tSide*4)
464- c := o.cursor
465- if _, err := c.setup(k, r); err != nil {
466- t.Fatal(err)
467- }
468- px := []byte{1, 2, 3, 4}
469- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil {
470- t.Fatal(err)
471- }
472- if mode == "allocation" {
473- r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once()
474- } else {
475- dumbFree(k)
476- rendererOK(t, k, r)
477- r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once()
478- }
479- if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 {
480- t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits))
481- }
482- if mode == "allocation" {
483- dumbFree(k)
484- } else {
485- k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
486- k.EXPECT().rmFB(uint32(81)).Return(nil).Once()
487- }
488- c.free()
489- })
490- }
491-}
@@ -1,491 +0,0 @@
1-package drm
2-
3-import (
4- "bytes"
5- "errors"
6- "os"
7- "testing"
8-
9- portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10- "github.com/bnema/neferwl/internal/ports"
11- "github.com/stretchr/testify/mock"
12- "golang.org/x/sys/unix"
13-)
14-
15-const tSide = 16
16-
17-// dumbOK expects two dumb buffers with handles 10 and 11, mappings (with
18-// pitch bytes per row) returned in mem, and framebuffers 90 and 91.
19-func dumbOK(k *mockkms, pitch int) (mem [2][]byte) {
20- for i := range mem {
21- mem[i] = make([]byte, pitch*tSide)
22- h := uint32(10 + i)
23- b := dumbBuffer{handle: h, pitch: uint32(pitch), size: uint64(len(mem[i]))}
24- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
25- k.EXPECT().mapDumb(b).Return(mem[i], nil).Once()
26- k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90+uint32(i), nil).Once()
27- }
28- return mem
29-}
30-
31-func dumbFree(k *mockkms) {
32- for i := range 2 {
33- k.EXPECT().rmFB(90 + uint32(i)).Return(nil).Once()
34- k.EXPECT().destroyDumb(10 + uint32(i)).Return(nil).Once()
35- }
36- k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice()
37-}
38-
39-func rendererBufs(t *testing.T) [2]ports.DMABuf {
40- var b [2]ports.DMABuf
41- for i := range b {
42- f, w, err := os.Pipe()
43- if err != nil {
44- t.Fatal(err)
45- }
46- t.Cleanup(func() { _ = f.Close(); _ = w.Close() })
47- if err := w.Close(); err != nil {
48- t.Fatal(err)
49- }
50- b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
51- }
52- return b
53-}
54-
55-func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) {
56- b := rendererBufs(t)
57- r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once()
58- t.Cleanup(func() { assertPlanesClosed(t, b) })
59- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once()
60- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once()
61-}
62-
63-// Dumb buffers come first: the renderer allocates nothing.
64-func TestCursorSetupUsesDumbBuffers(t *testing.T) {
65- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
66- dumbOK(k, tSide*4)
67- c := newCursor(&plane{}, tSide)
68- dumbErr, err := c.setup(k, r)
69- if dumbErr != nil || err != nil {
70- t.Fatalf("setup: %v %v", dumbErr, err)
71- }
72- if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} {
73- t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs)
74- }
75- dumbFree(k)
76- c.free()
77- if c.imgs != nil || c.fbs != [2]uint32{} {
78- t.Fatal("images kept after free")
79- }
80-}
81-
82-// A dumb buffer that cannot be made (or mapped, or wrapped) falls back to
83-// the renderer's images, and what was made is released.
84-func TestCursorSetupFallsBackToRenderer(t *testing.T) {
85- for name, fail := range map[string]func(k *mockkms){
86- "create": func(k *mockkms) {
87- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
88- },
89- "map": func(k *mockkms) {
90- b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
91- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
92- k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once()
93- k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
94- },
95- "framebuffer": func(k *mockkms) {
96- b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
97- mem := make([]byte, b.size)
98- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
99- k.EXPECT().mapDumb(b).Return(mem, nil).Once()
100- k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once()
101- k.EXPECT().unmapDumb(mem).Return(nil).Once()
102- k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
103- },
104- "second buffer": func(k *mockkms) {
105- b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
106- mem := make([]byte, b.size)
107- k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
108- k.EXPECT().mapDumb(b).Return(mem, nil).Once()
109- k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once()
110- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once()
111- k.EXPECT().rmFB(uint32(90)).Return(nil).Once()
112- k.EXPECT().unmapDumb(mem).Return(nil).Once()
113- k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
114- },
115- } {
116- t.Run(name, func(t *testing.T) {
117- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
118- fail(k)
119- rendererOK(t, k, r)
120- c := newCursor(&plane{}, tSide)
121- dumbErr, err := c.setup(k, r)
122- if dumbErr == nil || err != nil {
123- t.Fatalf("setup: %v %v", dumbErr, err)
124- }
125- if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} {
126- t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs)
127- }
128- })
129- }
130-}
131-
132-// Without either allocator there are no images.
133-func TestCursorSetupFailsWithoutAllocators(t *testing.T) {
134- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
135- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
136- r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once()
137- c := newCursor(&plane{}, tSide)
138- dumbErr, err := c.setup(k, r)
139- if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} {
140- t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs)
141- }
142- if _, err := c.useFallback(0); err != nil {
143- t.Fatal(err)
144- }
145-}
146-
147-// A renderer image whose framebuffer fails is released.
148-func TestRendererCursorReleasesOnFailure(t *testing.T) {
149- k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
150- bufs := rendererBufs(t)
151- r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once()
152- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once()
153- k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once()
154- k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
155- if _, err := newRendererCursor(k, r, tSide); err == nil {
156- t.Fatal("framebuffer failure not reported")
157- }
158- assertPlanesClosed(t, bufs)
159-}
160-
161-func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) {
162- t.Helper()
163- for i, b := range bufs {
164- for j, p := range b.Planes {
165- if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) {
166- t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err)
167- }
168- }
169- }
170-}
171-
172-// cursorOutput is a test output whose cursor plane has size tSide.
173-func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) {
174- o, k, commits := testOutput(t, errs...)
175- o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide)
176- return o, k, portsmocks.NewMockRenderer(t), commits
177-}
178-
179-// The plane refuses the dumb images: the renderer's are tried (TEST_ONLY),
180-// the image on the cursor is rewritten into them, and writes go there.
181-func TestCursorDumbRefusedUsesRendererImages(t *testing.T) {
182- o, k, r, commits := cursorOutput(t, unix.EINVAL)
183- dumbOK(k, tSide*4)
184- if _, err := o.cursor.setup(k, r); err != nil {
185- t.Fatal(err)
186- }
187- px := []byte{1, 2, 3, 4}
188- if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil {
189- t.Fatal(err)
190- }
191- dumbFree(k)
192- rendererOK(t, k, r)
193- var written []int
194- r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
195- written = append(written, i)
196- return nil
197- })
198- o.testCursor()
199- c := o.cursor
200- if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 {
201- t.Fatalf("off %v alloc %q fbs %v image %v hot %d,%d", c.off, c.alloc, c.fbs, c.image, c.hotX, c.hotY)
202- }
203- if len(written) != 1 || len(*commits) != 2 {
204- t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits))
205- }
206- if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly {
207- t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags)
208- }
209- // Further changes are written by the renderer, not into the freed buffers.
210- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 {
211- t.Fatalf("write after fallback: %v %v", err, written)
212- }
213-}
214-
215-// Both kinds refused: the software cursor takes over.
216-func TestCursorBothRefusedGoesOff(t *testing.T) {
217- o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL)
218- dumbOK(k, tSide*4)
219- if _, err := o.cursor.setup(k, r); err != nil {
220- t.Fatal(err)
221- }
222- dumbFree(k)
223- rendererOK(t, k, r)
224- if !o.testCursor() {
225- t.Fatal("no change reported")
226- }
227- if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 {
228- t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits))
229- }
230- if o.cursor.desired().on {
231- t.Fatal("cursor shown while off")
232- }
233- // The renderer's images are released with the output.
234- k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
235- k.EXPECT().rmFB(uint32(81)).Return(nil).Once()
236- o.cursor.free()
237-}
238-
239-// Dumb images the plane takes stay, with no second test.
240-func TestCursorDumbAcceptedStays(t *testing.T) {
241- for _, tc := range []struct {
242- name string
243- err error
244- }{{"accepted", nil}, {"non-refusal", unix.EIO}} {
245- t.Run(tc.name, func(t *testing.T) {
246- o, k, r, commits := cursorOutput(t, tc.err)
247- dumbOK(k, tSide*4)
248- if _, err := o.cursor.setup(k, r); err != nil {
249- t.Fatal(err)
250- }
251- if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 {
252- t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits))
253- }
254- dumbFree(k)
255- o.cursor.free()
256- })
257- }
258-}
259-
260-// Renderer images refused while no dumb ones exist: off, no retry.
261-func TestCursorRendererRefusedGoesOff(t *testing.T) {
262- o, k, r, commits := cursorOutput(t, unix.EINVAL)
263- k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
264- rendererOK(t, k, r)
265- if _, err := o.cursor.setup(k, r); err != nil {
266- t.Fatal(err)
267- }
268- if !o.testCursor() || !o.cursor.off || len(*commits) != 1 {
269- t.Fatalf("off %v commits %d", o.cursor.off, len(*commits))
270- }
271-}
272-
273-// Writes go to the images that own the cursor, and the output's HDR white
274-// is passed along.
275-func TestCursorWritesGoToCurrentImages(t *testing.T) {
276- o, _, _, _ := cursorOutput(t)
277- imgs := newMockcursorImages(t)
278- imgs.EXPECT().fb(0).Return(90).Once()
279- imgs.EXPECT().fb(1).Return(91).Once()
280- o.cursor.install(imgs, allocDumb)
281- px := []byte{1, 2, 3, 4}
282- imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once()
283- if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil {
284- t.Fatal(err)
285- }
286- o.hdr.on, o.hdr.settings.SDRBrightness = true, 203
287- if got := o.cursorNits(); got != 203 {
288- t.Fatalf("HDR white %v", got)
289- }
290- o.cursor.screen = 90
291- imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once()
292- if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil {
293- t.Fatal("write error swallowed")
294- }
295-}
296-
297-// Dumb pitch may exceed the row size: rows land at the pitch, the image is
298-// cropped to the side and cleared around.
299-func TestDumbCursorWritePitchCropClear(t *testing.T) {
300- k := newMockkms(t)
301- const pitch = tSide*4 + 32
302- mem := dumbOK(k, pitch)
303- d, err := newDumbCursor(k, tSide)
304- if err != nil {
305- t.Fatal(err)
306- }
307- defer func() { dumbFree(k); d.close() }()
308- big := make([]byte, 40*40*4)
309- for y := range 40 {
310- for x := range 40 {
311- copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255})
312- }
313- }
314- if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide
315- t.Fatal(err)
316- }
317- last := (tSide - 1) * pitch
318- for _, y := range []int{0, 1, 7, tSide - 1} {
319- for _, x := range []int{0, 3, tSide - 1} {
320- got := mem[1][y*pitch+x*4 : y*pitch+x*4+4]
321- want := big[(y*40+x)*4 : (y*40+x)*4+4]
322- if !bytes.Equal(got, want) {
323- t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want)
324- }
325- }
326- }
327- for y := range tSide {
328- if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) {
329- t.Fatalf("row %d padding written", y)
330- }
331- }
332- if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil {
333- t.Fatal(err)
334- }
335- m := mem[1]
336- if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 {
337- t.Fatal("image not cleared around the new pixels")
338- }
339- // A wide, short image keeps its rows apart.
340- row := []byte{9, 9, 9, 9, 8, 8, 8, 8}
341- if err := d.write(0, append(row, row...), 2, 2, 0); err != nil {
342- t.Fatal(err)
343- }
344- if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 {
345- t.Fatal("rows of a narrow image")
346- }
347- if err := d.write(0, []byte{1}, 1, 1, 0); err == nil {
348- t.Fatal("short image accepted")
349- }
350- if err := d.write(2, nil, 0, 0, 0); err == nil {
351- t.Fatal("bad index accepted")
352- }
353-}
354-
355-// HDR cursors convert exactly as the renderer did (see hdrcursor's vector).
356-func TestDumbCursorWriteHDR(t *testing.T) {
357- k := newMockkms(t)
358- mem := dumbOK(k, tSide*4)
359- d, err := newDumbCursor(k, tSide)
360- if err != nil {
361- t.Fatal(err)
362- }
363- defer func() { dumbFree(k); d.close() }()
364- if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil {
365- t.Fatal(err)
366- }
367- if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 {
368- t.Fatalf("HDR white: %v", m[:4])
369- }
370- if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 {
371- t.Fatalf("SDR white: %v %v", err, mem[1][:4])
372- }
373- if d.cache.Len() != 4 {
374- t.Fatalf("cache %d", d.cache.Len())
375- }
376-}
377-
378-func TestCursorFallbackState(t *testing.T) {
379- for _, mode := range []string{"loaded", "queued", "hidden"} {
380- t.Run(mode, func(t *testing.T) {
381- o, k, r, commits := cursorOutput(t, unix.EINVAL)
382- dumbOK(k, tSide*4)
383- c := o.cursor
384- if _, err := c.setup(k, r); err != nil {
385- t.Fatal(err)
386- }
387- px := []byte{1, 2, 3, 4}
388- if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil {
389- t.Fatal(err)
390- }
391- c.screen = 90
392- c.committed(cursorState{on: true, fb: 91, x: 8, y: 9})
393- want := []byte{1, 2, 3, 4}
394- if mode == "queued" {
395- px = []byte{5, 6, 7, 8}
396- want = bytes.Clone(px)
397- if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil {
398- t.Fatal(err)
399- }
400- if c.later == nil {
401- t.Fatal("image not queued")
402- }
403- }
404- if mode == "hidden" {
405- if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil {
406- t.Fatal(err)
407- }
408- }
409- px[0] = 99 // Neither the loaded nor queued image may alias caller memory.
410- dumbFree(k)
411- rendererOK(t, k, r)
412- if mode != "hidden" {
413- r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once()
414- }
415- if !o.testCursor() {
416- t.Fatal("swap not reported")
417- }
418- if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil {
419- t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied)
420- }
421- c.landed()
422- if c.screen != 0 {
423- t.Fatal("old pending flight resurrected")
424- }
425- s := c.desired()
426- if mode == "hidden" {
427- if s.on || c.image {
428- t.Fatal("hidden cursor shown")
429- }
430- } else {
431- if !s.on || s.fb != 80 {
432- t.Fatalf("desired %+v", s)
433- }
434- if mode == "queued" && (c.hotX != 4 || c.hotY != 5) {
435- t.Fatal("queued hotspot lost")
436- }
437- }
438- req := &atomicReq{}
439- c.props(req, o.crtc, s)
440- if err := k.commit(req, 0, 0); err != nil {
441- t.Fatal(err)
442- }
443- wantFB := uint64(80)
444- if mode == "hidden" {
445- wantFB = 0
446- }
447- if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB {
448- t.Fatalf("next commit fb %d want %d", fb, wantFB)
449- }
450- c.committed(s)
451- c.landed()
452- if c.screen != uint32(wantFB) {
453- t.Fatalf("screen %d", c.screen)
454- }
455- })
456- }
457-}
458-
459-func TestCursorFallbackFailureGoesOff(t *testing.T) {
460- for _, mode := range []string{"allocation", "rewrite"} {
461- t.Run(mode, func(t *testing.T) {
462- o, k, r, commits := cursorOutput(t, unix.EINVAL)
463- dumbOK(k, tSide*4)
464- c := o.cursor
465- if _, err := c.setup(k, r); err != nil {
466- t.Fatal(err)
467- }
468- px := []byte{1, 2, 3, 4}
469- if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil {
470- t.Fatal(err)
471- }
472- if mode == "allocation" {
473- r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once()
474- } else {
475- dumbFree(k)
476- rendererOK(t, k, r)
477- r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once()
478- }
479- if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 {
480- t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits))
481- }
482- if mode == "allocation" {
483- dumbFree(k)
484- } else {
485- k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
486- k.EXPECT().rmFB(uint32(81)).Return(nil).Once()
487- }
488- c.free()
489- })
490- }
491-}
D internal/adapters/drm/dumb.go +0 -77
@@ -1,77 +0,0 @@
1-package drm
2-
3-import (
4- "errors"
5- "fmt"
6- "unsafe"
7-
8- "golang.org/x/sys/unix"
9-)
10-
11-// KMS dumb buffers: the driver allocates a linear buffer the CPU maps and
12-// the display scans out. Cursor images use them (see cursor_images.go).
13-
14-const (
15- ioctlCreateDumb = 0xC02064B2 // DRM_IOWR('d', 0xB2, struct drm_mode_create_dumb)
16- ioctlMapDumb = 0xC01064B3 // DRM_IOWR('d', 0xB3, struct drm_mode_map_dumb)
17- ioctlDestroyDumb = 0xC00464B4 // DRM_IOWR('d', 0xB4, struct drm_mode_destroy_dumb)
18-)
19-
20-type createDumb struct {
21- height, width, bpp, flags uint32
22- handle, pitch uint32
23- size uint64
24-}
25-
26-type mapDumb struct {
27- handle, pad uint32
28- offset uint64
29-}
30-
31-// dumbBuffer is a dumb buffer's GEM handle, row pitch and size in bytes.
32-type dumbBuffer struct {
33- handle, pitch uint32
34- size uint64
35-}
36-
37-func (k kmsDevice) createDumb(width, height int) (dumbBuffer, error) {
38- if width <= 0 || height <= 0 {
39- return dumbBuffer{}, fmt.Errorf("dumb buffer size %dx%d", width, height)
40- }
41- c := createDumb{height: uint32(height), width: uint32(width), bpp: 32}
42- if err := ioctl(k.fd, ioctlCreateDumb, unsafe.Pointer(&c)); err != nil {
43- return dumbBuffer{}, err
44- }
45- return dumbBuffer{handle: c.handle, pitch: c.pitch, size: c.size}, nil
46-}
47-
48-func (k kmsDevice) mapDumb(b dumbBuffer) ([]byte, error) {
49- m := mapDumb{handle: b.handle}
50- if err := ioctl(k.fd, ioctlMapDumb, unsafe.Pointer(&m)); err != nil {
51- return nil, err
52- }
53- if b.size == 0 || b.size > 1<<30 {
54- return nil, fmt.Errorf("dumb buffer size %d", b.size)
55- }
56- return unix.Mmap(k.fd, int64(m.offset), int(b.size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
57-}
58-
59-func (k kmsDevice) unmapDumb(mem []byte) error { return unix.Munmap(mem) }
60-
61-// addFBHandle makes a framebuffer of format over the dumb buffer, without
62-// modifier (the driver's linear layout).
63-func (k kmsDevice) addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error) {
64- if b.handle == 0 {
65- return 0, errors.New("no dumb buffer")
66- }
67- cmd := fbCmd2{width: uint32(width), height: uint32(height), format: format}
68- cmd.handles[0], cmd.pitches[0] = b.handle, b.pitch
69- if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
70- return 0, errors.Join(errors.New("add fb2"), err)
71- }
72- return cmd.fbID, nil
73-}
74-
75-func (k kmsDevice) destroyDumb(handle uint32) error {
76- return ioctl(k.fd, ioctlDestroyDumb, unsafe.Pointer(&handle))
77-}
@@ -1,77 +0,0 @@
1-package drm
2-
3-import (
4- "errors"
5- "fmt"
6- "unsafe"
7-
8- "golang.org/x/sys/unix"
9-)
10-
11-// KMS dumb buffers: the driver allocates a linear buffer the CPU maps and
12-// the display scans out. Cursor images use them (see cursor_images.go).
13-
14-const (
15- ioctlCreateDumb = 0xC02064B2 // DRM_IOWR('d', 0xB2, struct drm_mode_create_dumb)
16- ioctlMapDumb = 0xC01064B3 // DRM_IOWR('d', 0xB3, struct drm_mode_map_dumb)
17- ioctlDestroyDumb = 0xC00464B4 // DRM_IOWR('d', 0xB4, struct drm_mode_destroy_dumb)
18-)
19-
20-type createDumb struct {
21- height, width, bpp, flags uint32
22- handle, pitch uint32
23- size uint64
24-}
25-
26-type mapDumb struct {
27- handle, pad uint32
28- offset uint64
29-}
30-
31-// dumbBuffer is a dumb buffer's GEM handle, row pitch and size in bytes.
32-type dumbBuffer struct {
33- handle, pitch uint32
34- size uint64
35-}
36-
37-func (k kmsDevice) createDumb(width, height int) (dumbBuffer, error) {
38- if width <= 0 || height <= 0 {
39- return dumbBuffer{}, fmt.Errorf("dumb buffer size %dx%d", width, height)
40- }
41- c := createDumb{height: uint32(height), width: uint32(width), bpp: 32}
42- if err := ioctl(k.fd, ioctlCreateDumb, unsafe.Pointer(&c)); err != nil {
43- return dumbBuffer{}, err
44- }
45- return dumbBuffer{handle: c.handle, pitch: c.pitch, size: c.size}, nil
46-}
47-
48-func (k kmsDevice) mapDumb(b dumbBuffer) ([]byte, error) {
49- m := mapDumb{handle: b.handle}
50- if err := ioctl(k.fd, ioctlMapDumb, unsafe.Pointer(&m)); err != nil {
51- return nil, err
52- }
53- if b.size == 0 || b.size > 1<<30 {
54- return nil, fmt.Errorf("dumb buffer size %d", b.size)
55- }
56- return unix.Mmap(k.fd, int64(m.offset), int(b.size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
57-}
58-
59-func (k kmsDevice) unmapDumb(mem []byte) error { return unix.Munmap(mem) }
60-
61-// addFBHandle makes a framebuffer of format over the dumb buffer, without
62-// modifier (the driver's linear layout).
63-func (k kmsDevice) addFBHandle(b dumbBuffer, width, height int, format uint32) (uint32, error) {
64- if b.handle == 0 {
65- return 0, errors.New("no dumb buffer")
66- }
67- cmd := fbCmd2{width: uint32(width), height: uint32(height), format: format}
68- cmd.handles[0], cmd.pitches[0] = b.handle, b.pitch
69- if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
70- return 0, errors.Join(errors.New("add fb2"), err)
71- }
72- return cmd.fbID, nil
73-}
74-
75-func (k kmsDevice) destroyDumb(handle uint32) error {
76- return ioctl(k.fd, ioctlDestroyDumb, unsafe.Pointer(&handle))
77-}
D internal/adapters/drm/dumb_device_test.go +0 -111
@@ -1,111 +0,0 @@
1-package drm
2-
3-import (
4- "bytes"
5- "errors"
6- "os"
7- "path/filepath"
8- "testing"
9-
10- "golang.org/x/sys/unix"
11-)
12-
13-// Real-device check of the dumb buffer layer: no modeset, no atomic commit.
14-// It skips when no card node can be opened or makes dumb buffers.
15-func TestDumbBuffersOnRealDevice(t *testing.T) {
16- cards, _ := filepath.Glob("/dev/dri/card*")
17- if len(cards) == 0 {
18- t.Skip("no /dev/dri/card* node")
19- }
20- for _, path := range cards {
21- f, err := os.OpenFile(path, os.O_RDWR|unix.O_CLOEXEC, 0)
22- if err != nil {
23- t.Logf("%s: %v", path, err)
24- continue
25- }
26- done := func() bool {
27- defer f.Close()
28- return dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())})
29- }()
30- if done {
31- return
32- }
33- }
34- t.Skip("no card node makes dumb buffers for this user")
35-}
36-
37-// dumbDeviceRoundTrip runs the buffer life cycle on k and reports false
38-// when the card cannot do it for a reason unrelated to the code under test
39-// (the permission or the driver).
40-func dumbDeviceRoundTrip(t *testing.T, path string, k kmsDevice) bool {
41- size := cursorSize(k.fd)
42- var imgs [2]dumbBuffer
43- var mems [2][]byte
44- var fbs [2]uint32
45- cleanup := func() {
46- for i := range imgs {
47- if fbs[i] != 0 {
48- if err := k.rmFB(fbs[i]); err != nil {
49- t.Errorf("rmfb: %v", err)
50- }
51- }
52- if mems[i] != nil {
53- if err := k.unmapDumb(mems[i]); err != nil {
54- t.Errorf("munmap: %v", err)
55- }
56- }
57- if imgs[i].handle != 0 {
58- if err := k.destroyDumb(imgs[i].handle); err != nil {
59- t.Errorf("destroy dumb: %v", err)
60- }
61- }
62- }
63- }
64- defer cleanup()
65- for i := range imgs {
66- b, err := k.createDumb(size, size)
67- if err != nil {
68- if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.ENOSYS) || errors.Is(err, unix.EINVAL) {
69- t.Logf("%s: create dumb: %v", path, err)
70- return false
71- }
72- t.Fatalf("%s: create dumb: %v", path, err)
73- }
74- imgs[i] = b
75- if int(b.pitch) < size*4 || b.size < uint64(int(b.pitch)*size) {
76- t.Fatalf("%s: dumb %dx%d pitch %d size %d", path, size, size, b.pitch, b.size)
77- }
78- if mems[i], err = k.mapDumb(b); err != nil {
79- t.Fatalf("%s: map dumb: %v", path, err)
80- }
81- if fbs[i], err = k.addFBHandle(b, size, size, fourccARGB); err != nil {
82- t.Fatalf("%s: add fb2 ARGB8888: %v", path, err)
83- }
84- t.Logf("%s: dumb buffer %d: %dx%d pitch %d size %d fb %d", path, i, size, size, b.pitch, b.size, fbs[i])
85- }
86- // Write a pattern through the shared mapping; each buffer keeps its own.
87- for i, mem := range mems {
88- d := &dumbCursor{k: k, size: size}
89- d.img[i] = dumbImage{pitch: int(imgs[i].pitch), mem: mem}
90- px := bytes.Repeat([]byte{byte(0x10 + i), 0x20, 0x30, 0xff}, 3*2)
91- if err := d.write(i, px, 3, 2, 0); err != nil {
92- t.Fatal(err)
93- }
94- }
95- for i, mem := range mems {
96- pitch := int(imgs[i].pitch)
97- want := []byte{byte(0x10 + i), 0x20, 0x30, 0xff}
98- for y := range 2 {
99- if got := mem[y*pitch : y*pitch+4]; !bytes.Equal(got, want) {
100- t.Fatalf("%s: buffer %d row %d read back %v, want %v", path, i, y, got, want)
101- }
102- if got := mem[y*pitch+3*4]; got != 0 {
103- t.Fatalf("%s: buffer %d row %d not cleared after the image", path, i, y)
104- }
105- }
106- if mem[2*pitch] != 0 || mem[(size-1)*pitch+(size-1)*4] != 0 {
107- t.Fatalf("%s: buffer %d not cleared below the image", path, i)
108- }
109- }
110- return true
111-}
@@ -1,111 +0,0 @@
1-package drm
2-
3-import (
4- "bytes"
5- "errors"
6- "os"
7- "path/filepath"
8- "testing"
9-
10- "golang.org/x/sys/unix"
11-)
12-
13-// Real-device check of the dumb buffer layer: no modeset, no atomic commit.
14-// It skips when no card node can be opened or makes dumb buffers.
15-func TestDumbBuffersOnRealDevice(t *testing.T) {
16- cards, _ := filepath.Glob("/dev/dri/card*")
17- if len(cards) == 0 {
18- t.Skip("no /dev/dri/card* node")
19- }
20- for _, path := range cards {
21- f, err := os.OpenFile(path, os.O_RDWR|unix.O_CLOEXEC, 0)
22- if err != nil {
23- t.Logf("%s: %v", path, err)
24- continue
25- }
26- done := func() bool {
27- defer f.Close()
28- return dumbDeviceRoundTrip(t, path, kmsDevice{fd: int(f.Fd())})
29- }()
30- if done {
31- return
32- }
33- }
34- t.Skip("no card node makes dumb buffers for this user")
35-}
36-
37-// dumbDeviceRoundTrip runs the buffer life cycle on k and reports false
38-// when the card cannot do it for a reason unrelated to the code under test
39-// (the permission or the driver).
40-func dumbDeviceRoundTrip(t *testing.T, path string, k kmsDevice) bool {
41- size := cursorSize(k.fd)
42- var imgs [2]dumbBuffer
43- var mems [2][]byte
44- var fbs [2]uint32
45- cleanup := func() {
46- for i := range imgs {
47- if fbs[i] != 0 {
48- if err := k.rmFB(fbs[i]); err != nil {
49- t.Errorf("rmfb: %v", err)
50- }
51- }
52- if mems[i] != nil {
53- if err := k.unmapDumb(mems[i]); err != nil {
54- t.Errorf("munmap: %v", err)
55- }
56- }
57- if imgs[i].handle != 0 {
58- if err := k.destroyDumb(imgs[i].handle); err != nil {
59- t.Errorf("destroy dumb: %v", err)
60- }
61- }
62- }
63- }
64- defer cleanup()
65- for i := range imgs {
66- b, err := k.createDumb(size, size)
67- if err != nil {
68- if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, unix.ENOTSUP) || errors.Is(err, unix.ENOSYS) || errors.Is(err, unix.EINVAL) {
69- t.Logf("%s: create dumb: %v", path, err)
70- return false
71- }
72- t.Fatalf("%s: create dumb: %v", path, err)
73- }
74- imgs[i] = b
75- if int(b.pitch) < size*4 || b.size < uint64(int(b.pitch)*size) {
76- t.Fatalf("%s: dumb %dx%d pitch %d size %d", path, size, size, b.pitch, b.size)
77- }
78- if mems[i], err = k.mapDumb(b); err != nil {
79- t.Fatalf("%s: map dumb: %v", path, err)
80- }
81- if fbs[i], err = k.addFBHandle(b, size, size, fourccARGB); err != nil {
82- t.Fatalf("%s: add fb2 ARGB8888: %v", path, err)
83- }
84- t.Logf("%s: dumb buffer %d: %dx%d pitch %d size %d fb %d", path, i, size, size, b.pitch, b.size, fbs[i])
85- }
86- // Write a pattern through the shared mapping; each buffer keeps its own.
87- for i, mem := range mems {
88- d := &dumbCursor{k: k, size: size}
89- d.img[i] = dumbImage{pitch: int(imgs[i].pitch), mem: mem}
90- px := bytes.Repeat([]byte{byte(0x10 + i), 0x20, 0x30, 0xff}, 3*2)
91- if err := d.write(i, px, 3, 2, 0); err != nil {
92- t.Fatal(err)
93- }
94- }
95- for i, mem := range mems {
96- pitch := int(imgs[i].pitch)
97- want := []byte{byte(0x10 + i), 0x20, 0x30, 0xff}
98- for y := range 2 {
99- if got := mem[y*pitch : y*pitch+4]; !bytes.Equal(got, want) {
100- t.Fatalf("%s: buffer %d row %d read back %v, want %v", path, i, y, got, want)
101- }
102- if got := mem[y*pitch+3*4]; got != 0 {
103- t.Fatalf("%s: buffer %d row %d not cleared after the image", path, i, y)
104- }
105- }
106- if mem[2*pitch] != 0 || mem[(size-1)*pitch+(size-1)*4] != 0 {
107- t.Fatalf("%s: buffer %d not cleared below the image", path, i)
108- }
109- }
110- return true
111-}
M internal/adapters/drm/frame_lifecycle.go +55 -4
@@ -13,6 +13,7 @@ import (
1313 // deadline ticks. Only Run uses the timer; the transitions take plain values.
1414 type frameLifecycle struct {
1515 pending bool
16+ waitKind frameWaitKind
1617 pendingFrame pendingFrame
1718 serials *atomic.Uint64 // shared across outputs on the card
1819 pendingSerial, nextSerial uint64
@@ -21,9 +22,22 @@ type frameLifecycle struct {
2122 stuckAt time.Time
2223 fenceWarned bool
2324 busySince, lastBusy time.Time
25+ nomemSince, lastNomem time.Time
26+ nomemBackoff time.Duration
27+ nomemAttempts int
2428 stuckTimer *time.Timer
2529 }
2630
31+// frameWaitKind is the current wait, independent of refusal-run summaries.
32+type frameWaitKind uint8
33+
34+const (
35+ waitNone frameWaitKind = iota
36+ waitFlip
37+ waitBusy
38+ waitNoMem
39+)
40+
2741 // pendingFrame is what the pending frame commit shows.
2842 type pendingFrame struct {
2943 security ports.SecurityState // admission epoch checked again at KMS boundary
@@ -66,8 +80,11 @@ const (
6680 )
6781
6882 const (
69- stuckTimeout = time.Second
70- busyRetry = 50 * time.Millisecond
83+ stuckTimeout = time.Second
84+ busyRetry = 50 * time.Millisecond
85+ nomemRetryMin = 16 * time.Millisecond
86+ nomemRetryMax = time.Second
87+ nomemWarnAfter = 5 * time.Second
7188 )
7289
7390 // stuckLimit returns the configured recovery deadline or its default.
@@ -88,6 +105,7 @@ func (l *frameLifecycle) userData(kind uint64) uint64 {
88105 func (l *frameLifecycle) endPending() {
89106 l.pendingFrame.closeFences()
90107 l.pending, l.pendingFrame = false, pendingFrame{}
108+ l.waitKind = waitNone
91109 }
92110
93111 // begin records a successful nonblocking commit awaiting its event.
@@ -95,6 +113,7 @@ func (l *frameLifecycle) begin(f pendingFrame, now time.Time) {
95113 l.pendingSerial = l.nextSerial
96114 l.pendingFrame.closeFences()
97115 l.pending, l.pendingFrame, l.flipStart = true, f, now
116+ l.waitKind = waitFlip
98117 l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false
99118 l.busySince = time.Time{}
100119 }
@@ -106,6 +125,33 @@ func (l *frameLifecycle) pendingCommit() bool { return l.pending }
106125 func (l *frameLifecycle) resetAfterModeset() {
107126 l.endPending()
108127 l.busySince = time.Time{}
128+ l.nomemRecovered(time.Time{})
129+}
130+
131+// nomem records a refused pin, not a missing flip. The old frame still shows.
132+func (l *frameLifecycle) nomem(now time.Time) (attempt int, newRun bool) {
133+ l.endPending()
134+ newRun = l.nomemSince.IsZero()
135+ if newRun {
136+ l.nomemSince = now
137+ }
138+ l.nomemBackoff = min(nomemRetryMax, max(nomemRetryMin, 2*l.nomemBackoff))
139+ l.lastNomem = now
140+ l.nomemAttempts++
141+ l.pending, l.pendingSerial, l.flipStart = true, 0, now
142+ l.waitKind = waitNoMem
143+ l.stuckAt = now.Add(l.nomemBackoff)
144+ return l.nomemAttempts, newRun
145+}
146+
147+// nomemRecovered consumes the refusal run after an accepted commit.
148+func (l *frameLifecycle) nomemRecovered(now time.Time) (attempts int, outage time.Duration) {
149+ if !l.nomemSince.IsZero() {
150+ attempts, outage = l.nomemAttempts, now.Sub(l.nomemSince)
151+ }
152+ l.nomemSince, l.lastNomem = time.Time{}, time.Time{}
153+ l.nomemBackoff, l.nomemAttempts = 0, 0
154+ return
109155 }
110156
111157 // deadlineReached reports whether a pending commit is ready for recovery.
@@ -126,6 +172,7 @@ func (l *frameLifecycle) busy(now time.Time) bool {
126172 l.endPending()
127173 l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)
128174 l.pendingSerial = 0
175+ l.waitKind = waitBusy
129176 newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry
130177 if newRun {
131178 l.busySince = now
@@ -144,7 +191,7 @@ func (l *frameLifecycle) ours(user uint64) bool {
144191 // EBUSY there is no known serial, so any CRTC event ends that wait. The
145192 // returned start lets Output measure flip age after pending fences close.
146193 func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {
147- if !l.pending || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
194+ if !l.pending || l.waitKind == waitNoMem || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
148195 return pendingFrame{}, time.Time{}, false
149196 }
150197 f, start := l.pendingFrame, l.flipStart
@@ -179,10 +226,14 @@ func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult
179226 }
180227 return timeoutEarly
181228 }
229+ kind := l.waitKind
182230 l.endPending()
183- if l.pendingSerial != 0 {
231+ if kind == waitFlip {
184232 return timeoutMissing
185233 }
234+ if kind == waitNoMem {
235+ return timeoutRetry
236+ }
186237 if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {
187238 return timeoutRetry
188239 }
@@ -13,6 +13,7 @@ import (
13 // deadline ticks. Only Run uses the timer; the transitions take plain values.13 // deadline ticks. Only Run uses the timer; the transitions take plain values.
14 type frameLifecycle struct {14 type frameLifecycle struct {
15 pending bool15 pending bool
16+ waitKind frameWaitKind
16 pendingFrame pendingFrame17 pendingFrame pendingFrame
17 serials *atomic.Uint64 // shared across outputs on the card18 serials *atomic.Uint64 // shared across outputs on the card
18 pendingSerial, nextSerial uint6419 pendingSerial, nextSerial uint64
@@ -21,9 +22,22 @@ type frameLifecycle struct {
21 stuckAt time.Time22 stuckAt time.Time
22 fenceWarned bool23 fenceWarned bool
23 busySince, lastBusy time.Time24 busySince, lastBusy time.Time
25+ nomemSince, lastNomem time.Time
26+ nomemBackoff time.Duration
27+ nomemAttempts int
24 stuckTimer *time.Timer28 stuckTimer *time.Timer
25 }29 }
26 30
31+// frameWaitKind is the current wait, independent of refusal-run summaries.
32+type frameWaitKind uint8
33+
34+const (
35+ waitNone frameWaitKind = iota
36+ waitFlip
37+ waitBusy
38+ waitNoMem
39+)
40+
27 // pendingFrame is what the pending frame commit shows.41 // pendingFrame is what the pending frame commit shows.
28 type pendingFrame struct {42 type pendingFrame struct {
29 security ports.SecurityState // admission epoch checked again at KMS boundary43 security ports.SecurityState // admission epoch checked again at KMS boundary
@@ -66,8 +80,11 @@ const (
66 )80 )
67 81
68 const (82 const (
69- stuckTimeout = time.Second83+ stuckTimeout = time.Second
70- busyRetry = 50 * time.Millisecond84+ busyRetry = 50 * time.Millisecond
85+ nomemRetryMin = 16 * time.Millisecond
86+ nomemRetryMax = time.Second
87+ nomemWarnAfter = 5 * time.Second
71 )88 )
72 89
73 // stuckLimit returns the configured recovery deadline or its default.90 // stuckLimit returns the configured recovery deadline or its default.
@@ -88,6 +105,7 @@ func (l *frameLifecycle) userData(kind uint64) uint64 {
88 func (l *frameLifecycle) endPending() {105 func (l *frameLifecycle) endPending() {
89 l.pendingFrame.closeFences()106 l.pendingFrame.closeFences()
90 l.pending, l.pendingFrame = false, pendingFrame{}107 l.pending, l.pendingFrame = false, pendingFrame{}
108+ l.waitKind = waitNone
91 }109 }
92 110
93 // begin records a successful nonblocking commit awaiting its event.111 // begin records a successful nonblocking commit awaiting its event.
@@ -95,6 +113,7 @@ func (l *frameLifecycle) begin(f pendingFrame, now time.Time) {
95 l.pendingSerial = l.nextSerial113 l.pendingSerial = l.nextSerial
96 l.pendingFrame.closeFences()114 l.pendingFrame.closeFences()
97 l.pending, l.pendingFrame, l.flipStart = true, f, now115 l.pending, l.pendingFrame, l.flipStart = true, f, now
116+ l.waitKind = waitFlip
98 l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false117 l.stuckAt, l.fenceWarned = now.Add(l.stuckLimit()), false
99 l.busySince = time.Time{}118 l.busySince = time.Time{}
100 }119 }
@@ -106,6 +125,33 @@ func (l *frameLifecycle) pendingCommit() bool { return l.pending }
106 func (l *frameLifecycle) resetAfterModeset() {125 func (l *frameLifecycle) resetAfterModeset() {
107 l.endPending()126 l.endPending()
108 l.busySince = time.Time{}127 l.busySince = time.Time{}
128+ l.nomemRecovered(time.Time{})
129+}
130+
131+// nomem records a refused pin, not a missing flip. The old frame still shows.
132+func (l *frameLifecycle) nomem(now time.Time) (attempt int, newRun bool) {
133+ l.endPending()
134+ newRun = l.nomemSince.IsZero()
135+ if newRun {
136+ l.nomemSince = now
137+ }
138+ l.nomemBackoff = min(nomemRetryMax, max(nomemRetryMin, 2*l.nomemBackoff))
139+ l.lastNomem = now
140+ l.nomemAttempts++
141+ l.pending, l.pendingSerial, l.flipStart = true, 0, now
142+ l.waitKind = waitNoMem
143+ l.stuckAt = now.Add(l.nomemBackoff)
144+ return l.nomemAttempts, newRun
145+}
146+
147+// nomemRecovered consumes the refusal run after an accepted commit.
148+func (l *frameLifecycle) nomemRecovered(now time.Time) (attempts int, outage time.Duration) {
149+ if !l.nomemSince.IsZero() {
150+ attempts, outage = l.nomemAttempts, now.Sub(l.nomemSince)
151+ }
152+ l.nomemSince, l.lastNomem = time.Time{}, time.Time{}
153+ l.nomemBackoff, l.nomemAttempts = 0, 0
154+ return
109 }155 }
110 156
111 // deadlineReached reports whether a pending commit is ready for recovery.157 // deadlineReached reports whether a pending commit is ready for recovery.
@@ -126,6 +172,7 @@ func (l *frameLifecycle) busy(now time.Time) bool {
126 l.endPending()172 l.endPending()
127 l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)173 l.pending, l.flipStart, l.stuckAt = true, now, now.Add(busyRetry)
128 l.pendingSerial = 0174 l.pendingSerial = 0
175+ l.waitKind = waitBusy
129 newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry176 newRun := l.busySince.IsZero() || now.Sub(l.lastBusy) > 4*busyRetry
130 if newRun {177 if newRun {
131 l.busySince = now178 l.busySince = now
@@ -144,7 +191,7 @@ func (l *frameLifecycle) ours(user uint64) bool {
144 // EBUSY there is no known serial, so any CRTC event ends that wait. The191 // EBUSY there is no known serial, so any CRTC event ends that wait. The
145 // returned start lets Output measure flip age after pending fences close.192 // returned start lets Output measure flip age after pending fences close.
146 func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {193 func (l *frameLifecycle) flip(user uint64) (pendingFrame, time.Time, bool) {
147- if !l.pending || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {194+ if !l.pending || l.waitKind == waitNoMem || l.pendingSerial != 0 && user>>userKindBits != l.pendingSerial {
148 return pendingFrame{}, time.Time{}, false195 return pendingFrame{}, time.Time{}, false
149 }196 }
150 f, start := l.pendingFrame, l.flipStart197 f, start := l.pendingFrame, l.flipStart
@@ -179,10 +226,14 @@ func (l *frameLifecycle) timeout(now time.Time, fencesReady bool) timeoutResult
179 }226 }
180 return timeoutEarly227 return timeoutEarly
181 }228 }
229+ kind := l.waitKind
182 l.endPending()230 l.endPending()
183- if l.pendingSerial != 0 {231+ if kind == waitFlip {
184 return timeoutMissing232 return timeoutMissing
185 }233 }
234+ if kind == waitNoMem {
235+ return timeoutRetry
236+ }
186 if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {237 if l.busySince.IsZero() || now.Sub(l.busySince) < l.stuckLimit() {
187 return timeoutRetry238 return timeoutRetry
188 }239 }
M internal/adapters/drm/frame_lifecycle_test.go +66 -0
@@ -95,3 +95,69 @@ func TestFrameLifecycleTransitionsAllocations(t *testing.T) {
9595 t.Fatalf("frame lifecycle: %.1f allocs, want 0", allocs)
9696 }
9797 }
98+
99+func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) {
100+ l := frameLifecycle{stuckAfter: time.Millisecond}
101+ start := time.Unix(100, 0)
102+ now := start
103+ for i := range 20 {
104+ attempt, fresh := l.nomem(now)
105+ want := min(nomemRetryMax, nomemRetryMin*time.Duration(1<<min(i, 7)))
106+ if attempt != i+1 || fresh != (i == 0) || l.nomemBackoff != want || l.stuckAt != now.Add(want) {
107+ t.Fatalf("attempt %d fresh=%v backoff=%v deadline=%v", attempt, fresh, l.nomemBackoff, l.stuckAt)
108+ }
109+ if _, _, ok := l.flip(123 << userKindBits); ok {
110+ t.Fatal("stale event bypassed memory backoff")
111+ }
112+ if got := l.timeout(l.stuckAt.Add(10*l.stuckLimit()), true); got != timeoutRetry {
113+ t.Fatalf("ENOMEM escalated: %d", got)
114+ }
115+ now = l.stuckAt
116+ }
117+ attempts, outage := l.nomemRecovered(now)
118+ if attempts != 20 || outage != now.Sub(start) || !l.nomemSince.IsZero() || !l.lastNomem.IsZero() || l.nomemBackoff != 0 || l.nomemAttempts != 0 {
119+ t.Fatalf("recovery: %d %v %+v", attempts, outage, l)
120+ }
121+ l.nomem(now)
122+ l.resetAfterModeset()
123+ if !l.nomemSince.IsZero() || l.nomemAttempts != 0 || l.pending {
124+ t.Fatal("modeset did not clear run")
125+ }
126+}
127+
128+func TestFrameLifecycleNoMemThenBusyEvent(t *testing.T) {
129+ l := frameLifecycle{}
130+ start := time.Unix(100, 0)
131+ l.nomem(start)
132+ if got := l.timeout(l.stuckAt, true); got != timeoutRetry {
133+ t.Fatalf("memory wait: %v", got)
134+ }
135+ l.busy(start.Add(nomemRetryMin))
136+ if _, _, ok := l.flip(123 << userKindBits); !ok || l.pending || l.waitKind != waitNone {
137+ t.Fatal("memory history prevented a CRTC event from ending EBUSY")
138+ }
139+ if l.nomemAttempts != 1 || l.nomemSince != start {
140+ t.Fatal("busy event lost memory recovery summary")
141+ }
142+}
143+
144+func TestFrameLifecycleNoMemThenBusyLimit(t *testing.T) {
145+ l := frameLifecycle{stuckAfter: 200 * time.Millisecond}
146+ start := time.Unix(100, 0)
147+ l.nomem(start)
148+ l.timeout(l.stuckAt, true)
149+ busyStart := start.Add(nomemRetryMin)
150+ for i := range 4 {
151+ l.busy(busyStart.Add(time.Duration(i) * busyRetry))
152+ want := timeoutRetry
153+ if i == 3 {
154+ want = timeoutBusy
155+ }
156+ if got := l.timeout(l.stuckAt, true); got != want {
157+ t.Fatalf("busy attempt %d: got %v want %v", i+1, got, want)
158+ }
159+ }
160+ if l.nomemAttempts != 1 || l.nomemSince != start {
161+ t.Fatal("busy timeout lost memory recovery summary")
162+ }
163+}
@@ -95,3 +95,69 @@ func TestFrameLifecycleTransitionsAllocations(t *testing.T) {
95 t.Fatalf("frame lifecycle: %.1f allocs, want 0", allocs)95 t.Fatalf("frame lifecycle: %.1f allocs, want 0", allocs)
96 }96 }
97 }97 }
98+
99+func TestFrameLifecycleNoMemBackoffAndRecovery(t *testing.T) {
100+ l := frameLifecycle{stuckAfter: time.Millisecond}
101+ start := time.Unix(100, 0)
102+ now := start
103+ for i := range 20 {
104+ attempt, fresh := l.nomem(now)
105+ want := min(nomemRetryMax, nomemRetryMin*time.Duration(1<<min(i, 7)))
106+ if attempt != i+1 || fresh != (i == 0) || l.nomemBackoff != want || l.stuckAt != now.Add(want) {
107+ t.Fatalf("attempt %d fresh=%v backoff=%v deadline=%v", attempt, fresh, l.nomemBackoff, l.stuckAt)
108+ }
109+ if _, _, ok := l.flip(123 << userKindBits); ok {
110+ t.Fatal("stale event bypassed memory backoff")
111+ }
112+ if got := l.timeout(l.stuckAt.Add(10*l.stuckLimit()), true); got != timeoutRetry {
113+ t.Fatalf("ENOMEM escalated: %d", got)
114+ }
115+ now = l.stuckAt
116+ }
117+ attempts, outage := l.nomemRecovered(now)
118+ if attempts != 20 || outage != now.Sub(start) || !l.nomemSince.IsZero() || !l.lastNomem.IsZero() || l.nomemBackoff != 0 || l.nomemAttempts != 0 {
119+ t.Fatalf("recovery: %d %v %+v", attempts, outage, l)
120+ }
121+ l.nomem(now)
122+ l.resetAfterModeset()
123+ if !l.nomemSince.IsZero() || l.nomemAttempts != 0 || l.pending {
124+ t.Fatal("modeset did not clear run")
125+ }
126+}
127+
128+func TestFrameLifecycleNoMemThenBusyEvent(t *testing.T) {
129+ l := frameLifecycle{}
130+ start := time.Unix(100, 0)
131+ l.nomem(start)
132+ if got := l.timeout(l.stuckAt, true); got != timeoutRetry {
133+ t.Fatalf("memory wait: %v", got)
134+ }
135+ l.busy(start.Add(nomemRetryMin))
136+ if _, _, ok := l.flip(123 << userKindBits); !ok || l.pending || l.waitKind != waitNone {
137+ t.Fatal("memory history prevented a CRTC event from ending EBUSY")
138+ }
139+ if l.nomemAttempts != 1 || l.nomemSince != start {
140+ t.Fatal("busy event lost memory recovery summary")
141+ }
142+}
143+
144+func TestFrameLifecycleNoMemThenBusyLimit(t *testing.T) {
145+ l := frameLifecycle{stuckAfter: 200 * time.Millisecond}
146+ start := time.Unix(100, 0)
147+ l.nomem(start)
148+ l.timeout(l.stuckAt, true)
149+ busyStart := start.Add(nomemRetryMin)
150+ for i := range 4 {
151+ l.busy(busyStart.Add(time.Duration(i) * busyRetry))
152+ want := timeoutRetry
153+ if i == 3 {
154+ want = timeoutBusy
155+ }
156+ if got := l.timeout(l.stuckAt, true); got != want {
157+ t.Fatalf("busy attempt %d: got %v want %v", i+1, got, want)
158+ }
159+ }
160+ if l.nomemAttempts != 1 || l.nomemSince != start {
161+ t.Fatal("busy timeout lost memory recovery summary")
162+ }
163+}
M internal/adapters/drm/kms_mock_test.go +0 -308
@@ -113,84 +113,6 @@ func (_c *mockkms_addFB_Call) RunAndReturn(run func(b *ports.DMABuf, format uint
113113 return _c
114114 }
115115
116-// addFBHandle provides a mock function for the type mockkms
117-func (_mock *mockkms) addFBHandle(b dumbBuffer, width int, height int, format uint32) (uint32, error) {
118- ret := _mock.Called(b, width, height, format)
119-
120- if len(ret) == 0 {
121- panic("no return value specified for addFBHandle")
122- }
123-
124- var r0 uint32
125- var r1 error
126- if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) (uint32, error)); ok {
127- return returnFunc(b, width, height, format)
128- }
129- if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) uint32); ok {
130- r0 = returnFunc(b, width, height, format)
131- } else {
132- r0 = ret.Get(0).(uint32)
133- }
134- if returnFunc, ok := ret.Get(1).(func(dumbBuffer, int, int, uint32) error); ok {
135- r1 = returnFunc(b, width, height, format)
136- } else {
137- r1 = ret.Error(1)
138- }
139- return r0, r1
140-}
141-
142-// mockkms_addFBHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'addFBHandle'
143-type mockkms_addFBHandle_Call struct {
144- *mock.Call
145-}
146-
147-// addFBHandle is a helper method to define mock.On call
148-// - b dumbBuffer
149-// - width int
150-// - height int
151-// - format uint32
152-func (_e *mockkms_Expecter) addFBHandle(b any, width any, height any, format any) *mockkms_addFBHandle_Call {
153- return &mockkms_addFBHandle_Call{Call: _e.mock.On("addFBHandle", b, width, height, format)}
154-}
155-
156-func (_c *mockkms_addFBHandle_Call) Run(run func(b dumbBuffer, width int, height int, format uint32)) *mockkms_addFBHandle_Call {
157- _c.Call.Run(func(args mock.Arguments) {
158- var arg0 dumbBuffer
159- if args[0] != nil {
160- arg0 = args[0].(dumbBuffer)
161- }
162- var arg1 int
163- if args[1] != nil {
164- arg1 = args[1].(int)
165- }
166- var arg2 int
167- if args[2] != nil {
168- arg2 = args[2].(int)
169- }
170- var arg3 uint32
171- if args[3] != nil {
172- arg3 = args[3].(uint32)
173- }
174- run(
175- arg0,
176- arg1,
177- arg2,
178- arg3,
179- )
180- })
181- return _c
182-}
183-
184-func (_c *mockkms_addFBHandle_Call) Return(v uint32, err error) *mockkms_addFBHandle_Call {
185- _c.Call.Return(v, err)
186- return _c
187-}
188-
189-func (_c *mockkms_addFBHandle_Call) RunAndReturn(run func(b dumbBuffer, width int, height int, format uint32) (uint32, error)) *mockkms_addFBHandle_Call {
190- _c.Call.Return(run)
191- return _c
192-}
193-
194116 // commit provides a mock function for the type mockkms
195117 func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error {
196118 ret := _mock.Called(req, flags, userData)
@@ -374,72 +296,6 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e
374296 return _c
375297 }
376298
377-// createDumb provides a mock function for the type mockkms
378-func (_mock *mockkms) createDumb(width int, height int) (dumbBuffer, error) {
379- ret := _mock.Called(width, height)
380-
381- if len(ret) == 0 {
382- panic("no return value specified for createDumb")
383- }
384-
385- var r0 dumbBuffer
386- var r1 error
387- if returnFunc, ok := ret.Get(0).(func(int, int) (dumbBuffer, error)); ok {
388- return returnFunc(width, height)
389- }
390- if returnFunc, ok := ret.Get(0).(func(int, int) dumbBuffer); ok {
391- r0 = returnFunc(width, height)
392- } else {
393- r0 = ret.Get(0).(dumbBuffer)
394- }
395- if returnFunc, ok := ret.Get(1).(func(int, int) error); ok {
396- r1 = returnFunc(width, height)
397- } else {
398- r1 = ret.Error(1)
399- }
400- return r0, r1
401-}
402-
403-// mockkms_createDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createDumb'
404-type mockkms_createDumb_Call struct {
405- *mock.Call
406-}
407-
408-// createDumb is a helper method to define mock.On call
409-// - width int
410-// - height int
411-func (_e *mockkms_Expecter) createDumb(width any, height any) *mockkms_createDumb_Call {
412- return &mockkms_createDumb_Call{Call: _e.mock.On("createDumb", width, height)}
413-}
414-
415-func (_c *mockkms_createDumb_Call) Run(run func(width int, height int)) *mockkms_createDumb_Call {
416- _c.Call.Run(func(args mock.Arguments) {
417- var arg0 int
418- if args[0] != nil {
419- arg0 = args[0].(int)
420- }
421- var arg1 int
422- if args[1] != nil {
423- arg1 = args[1].(int)
424- }
425- run(
426- arg0,
427- arg1,
428- )
429- })
430- return _c
431-}
432-
433-func (_c *mockkms_createDumb_Call) Return(dumbBufferMoqParam dumbBuffer, err error) *mockkms_createDumb_Call {
434- _c.Call.Return(dumbBufferMoqParam, err)
435- return _c
436-}
437-
438-func (_c *mockkms_createDumb_Call) RunAndReturn(run func(width int, height int) (dumbBuffer, error)) *mockkms_createDumb_Call {
439- _c.Call.Return(run)
440- return _c
441-}
442-
443299 // createLease provides a mock function for the type mockkms
444300 func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) {
445301 ret := _mock.Called(objects)
@@ -559,57 +415,6 @@ func (_c *mockkms_destroyBlob_Call) RunAndReturn(run func(id uint32) error) *moc
559415 return _c
560416 }
561417
562-// destroyDumb provides a mock function for the type mockkms
563-func (_mock *mockkms) destroyDumb(handle uint32) error {
564- ret := _mock.Called(handle)
565-
566- if len(ret) == 0 {
567- panic("no return value specified for destroyDumb")
568- }
569-
570- var r0 error
571- if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
572- r0 = returnFunc(handle)
573- } else {
574- r0 = ret.Error(0)
575- }
576- return r0
577-}
578-
579-// mockkms_destroyDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroyDumb'
580-type mockkms_destroyDumb_Call struct {
581- *mock.Call
582-}
583-
584-// destroyDumb is a helper method to define mock.On call
585-// - handle uint32
586-func (_e *mockkms_Expecter) destroyDumb(handle any) *mockkms_destroyDumb_Call {
587- return &mockkms_destroyDumb_Call{Call: _e.mock.On("destroyDumb", handle)}
588-}
589-
590-func (_c *mockkms_destroyDumb_Call) Run(run func(handle uint32)) *mockkms_destroyDumb_Call {
591- _c.Call.Run(func(args mock.Arguments) {
592- var arg0 uint32
593- if args[0] != nil {
594- arg0 = args[0].(uint32)
595- }
596- run(
597- arg0,
598- )
599- })
600- return _c
601-}
602-
603-func (_c *mockkms_destroyDumb_Call) Return(err error) *mockkms_destroyDumb_Call {
604- _c.Call.Return(err)
605- return _c
606-}
607-
608-func (_c *mockkms_destroyDumb_Call) RunAndReturn(run func(handle uint32) error) *mockkms_destroyDumb_Call {
609- _c.Call.Return(run)
610- return _c
611-}
612-
613418 // getBlob provides a mock function for the type mockkms
614419 func (_mock *mockkms) getBlob(id uint32) ([]byte, error) {
615420 ret := _mock.Called(id)
@@ -672,68 +477,6 @@ func (_c *mockkms_getBlob_Call) RunAndReturn(run func(id uint32) ([]byte, error)
672477 return _c
673478 }
674479
675-// mapDumb provides a mock function for the type mockkms
676-func (_mock *mockkms) mapDumb(b dumbBuffer) ([]byte, error) {
677- ret := _mock.Called(b)
678-
679- if len(ret) == 0 {
680- panic("no return value specified for mapDumb")
681- }
682-
683- var r0 []byte
684- var r1 error
685- if returnFunc, ok := ret.Get(0).(func(dumbBuffer) ([]byte, error)); ok {
686- return returnFunc(b)
687- }
688- if returnFunc, ok := ret.Get(0).(func(dumbBuffer) []byte); ok {
689- r0 = returnFunc(b)
690- } else {
691- if ret.Get(0) != nil {
692- r0 = ret.Get(0).([]byte)
693- }
694- }
695- if returnFunc, ok := ret.Get(1).(func(dumbBuffer) error); ok {
696- r1 = returnFunc(b)
697- } else {
698- r1 = ret.Error(1)
699- }
700- return r0, r1
701-}
702-
703-// mockkms_mapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'mapDumb'
704-type mockkms_mapDumb_Call struct {
705- *mock.Call
706-}
707-
708-// mapDumb is a helper method to define mock.On call
709-// - b dumbBuffer
710-func (_e *mockkms_Expecter) mapDumb(b any) *mockkms_mapDumb_Call {
711- return &mockkms_mapDumb_Call{Call: _e.mock.On("mapDumb", b)}
712-}
713-
714-func (_c *mockkms_mapDumb_Call) Run(run func(b dumbBuffer)) *mockkms_mapDumb_Call {
715- _c.Call.Run(func(args mock.Arguments) {
716- var arg0 dumbBuffer
717- if args[0] != nil {
718- arg0 = args[0].(dumbBuffer)
719- }
720- run(
721- arg0,
722- )
723- })
724- return _c
725-}
726-
727-func (_c *mockkms_mapDumb_Call) Return(bytes []byte, err error) *mockkms_mapDumb_Call {
728- _c.Call.Return(bytes, err)
729- return _c
730-}
731-
732-func (_c *mockkms_mapDumb_Call) RunAndReturn(run func(b dumbBuffer) ([]byte, error)) *mockkms_mapDumb_Call {
733- _c.Call.Return(run)
734- return _c
735-}
736-
737480 // objProps provides a mock function for the type mockkms
738481 func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) {
739482 ret := _mock.Called(obj, typ)
@@ -1155,54 +898,3 @@ func (_c *mockkms_rmFB_Call) RunAndReturn(run func(id uint32) error) *mockkms_rm
1155898 _c.Call.Return(run)
1156899 return _c
1157900 }
1158-
1159-// unmapDumb provides a mock function for the type mockkms
1160-func (_mock *mockkms) unmapDumb(mem []byte) error {
1161- ret := _mock.Called(mem)
1162-
1163- if len(ret) == 0 {
1164- panic("no return value specified for unmapDumb")
1165- }
1166-
1167- var r0 error
1168- if returnFunc, ok := ret.Get(0).(func([]byte) error); ok {
1169- r0 = returnFunc(mem)
1170- } else {
1171- r0 = ret.Error(0)
1172- }
1173- return r0
1174-}
1175-
1176-// mockkms_unmapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'unmapDumb'
1177-type mockkms_unmapDumb_Call struct {
1178- *mock.Call
1179-}
1180-
1181-// unmapDumb is a helper method to define mock.On call
1182-// - mem []byte
1183-func (_e *mockkms_Expecter) unmapDumb(mem any) *mockkms_unmapDumb_Call {
1184- return &mockkms_unmapDumb_Call{Call: _e.mock.On("unmapDumb", mem)}
1185-}
1186-
1187-func (_c *mockkms_unmapDumb_Call) Run(run func(mem []byte)) *mockkms_unmapDumb_Call {
1188- _c.Call.Run(func(args mock.Arguments) {
1189- var arg0 []byte
1190- if args[0] != nil {
1191- arg0 = args[0].([]byte)
1192- }
1193- run(
1194- arg0,
1195- )
1196- })
1197- return _c
1198-}
1199-
1200-func (_c *mockkms_unmapDumb_Call) Return(err error) *mockkms_unmapDumb_Call {
1201- _c.Call.Return(err)
1202- return _c
1203-}
1204-
1205-func (_c *mockkms_unmapDumb_Call) RunAndReturn(run func(mem []byte) error) *mockkms_unmapDumb_Call {
1206- _c.Call.Return(run)
1207- return _c
1208-}
@@ -113,84 +113,6 @@ func (_c *mockkms_addFB_Call) RunAndReturn(run func(b *ports.DMABuf, format uint
113 return _c113 return _c
114 }114 }
115 115
116-// addFBHandle provides a mock function for the type mockkms
117-func (_mock *mockkms) addFBHandle(b dumbBuffer, width int, height int, format uint32) (uint32, error) {
118- ret := _mock.Called(b, width, height, format)
119-
120- if len(ret) == 0 {
121- panic("no return value specified for addFBHandle")
122- }
123-
124- var r0 uint32
125- var r1 error
126- if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) (uint32, error)); ok {
127- return returnFunc(b, width, height, format)
128- }
129- if returnFunc, ok := ret.Get(0).(func(dumbBuffer, int, int, uint32) uint32); ok {
130- r0 = returnFunc(b, width, height, format)
131- } else {
132- r0 = ret.Get(0).(uint32)
133- }
134- if returnFunc, ok := ret.Get(1).(func(dumbBuffer, int, int, uint32) error); ok {
135- r1 = returnFunc(b, width, height, format)
136- } else {
137- r1 = ret.Error(1)
138- }
139- return r0, r1
140-}
141-
142-// mockkms_addFBHandle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'addFBHandle'
143-type mockkms_addFBHandle_Call struct {
144- *mock.Call
145-}
146-
147-// addFBHandle is a helper method to define mock.On call
148-// - b dumbBuffer
149-// - width int
150-// - height int
151-// - format uint32
152-func (_e *mockkms_Expecter) addFBHandle(b any, width any, height any, format any) *mockkms_addFBHandle_Call {
153- return &mockkms_addFBHandle_Call{Call: _e.mock.On("addFBHandle", b, width, height, format)}
154-}
155-
156-func (_c *mockkms_addFBHandle_Call) Run(run func(b dumbBuffer, width int, height int, format uint32)) *mockkms_addFBHandle_Call {
157- _c.Call.Run(func(args mock.Arguments) {
158- var arg0 dumbBuffer
159- if args[0] != nil {
160- arg0 = args[0].(dumbBuffer)
161- }
162- var arg1 int
163- if args[1] != nil {
164- arg1 = args[1].(int)
165- }
166- var arg2 int
167- if args[2] != nil {
168- arg2 = args[2].(int)
169- }
170- var arg3 uint32
171- if args[3] != nil {
172- arg3 = args[3].(uint32)
173- }
174- run(
175- arg0,
176- arg1,
177- arg2,
178- arg3,
179- )
180- })
181- return _c
182-}
183-
184-func (_c *mockkms_addFBHandle_Call) Return(v uint32, err error) *mockkms_addFBHandle_Call {
185- _c.Call.Return(v, err)
186- return _c
187-}
188-
189-func (_c *mockkms_addFBHandle_Call) RunAndReturn(run func(b dumbBuffer, width int, height int, format uint32) (uint32, error)) *mockkms_addFBHandle_Call {
190- _c.Call.Return(run)
191- return _c
192-}
193-
194 // commit provides a mock function for the type mockkms116 // commit provides a mock function for the type mockkms
195 func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error {117 func (_mock *mockkms) commit(req *atomicReq, flags uint32, userData uint64) error {
196 ret := _mock.Called(req, flags, userData)118 ret := _mock.Called(req, flags, userData)
@@ -374,72 +296,6 @@ func (_c *mockkms_createBlob_Call) RunAndReturn(run func(data []byte) (uint32, e
374 return _c296 return _c
375 }297 }
376 298
377-// createDumb provides a mock function for the type mockkms
378-func (_mock *mockkms) createDumb(width int, height int) (dumbBuffer, error) {
379- ret := _mock.Called(width, height)
380-
381- if len(ret) == 0 {
382- panic("no return value specified for createDumb")
383- }
384-
385- var r0 dumbBuffer
386- var r1 error
387- if returnFunc, ok := ret.Get(0).(func(int, int) (dumbBuffer, error)); ok {
388- return returnFunc(width, height)
389- }
390- if returnFunc, ok := ret.Get(0).(func(int, int) dumbBuffer); ok {
391- r0 = returnFunc(width, height)
392- } else {
393- r0 = ret.Get(0).(dumbBuffer)
394- }
395- if returnFunc, ok := ret.Get(1).(func(int, int) error); ok {
396- r1 = returnFunc(width, height)
397- } else {
398- r1 = ret.Error(1)
399- }
400- return r0, r1
401-}
402-
403-// mockkms_createDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'createDumb'
404-type mockkms_createDumb_Call struct {
405- *mock.Call
406-}
407-
408-// createDumb is a helper method to define mock.On call
409-// - width int
410-// - height int
411-func (_e *mockkms_Expecter) createDumb(width any, height any) *mockkms_createDumb_Call {
412- return &mockkms_createDumb_Call{Call: _e.mock.On("createDumb", width, height)}
413-}
414-
415-func (_c *mockkms_createDumb_Call) Run(run func(width int, height int)) *mockkms_createDumb_Call {
416- _c.Call.Run(func(args mock.Arguments) {
417- var arg0 int
418- if args[0] != nil {
419- arg0 = args[0].(int)
420- }
421- var arg1 int
422- if args[1] != nil {
423- arg1 = args[1].(int)
424- }
425- run(
426- arg0,
427- arg1,
428- )
429- })
430- return _c
431-}
432-
433-func (_c *mockkms_createDumb_Call) Return(dumbBufferMoqParam dumbBuffer, err error) *mockkms_createDumb_Call {
434- _c.Call.Return(dumbBufferMoqParam, err)
435- return _c
436-}
437-
438-func (_c *mockkms_createDumb_Call) RunAndReturn(run func(width int, height int) (dumbBuffer, error)) *mockkms_createDumb_Call {
439- _c.Call.Return(run)
440- return _c
441-}
442-
443 // createLease provides a mock function for the type mockkms299 // createLease provides a mock function for the type mockkms
444 func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) {300 func (_mock *mockkms) createLease(objects []uint32) (*os.File, uint32, error) {
445 ret := _mock.Called(objects)301 ret := _mock.Called(objects)
@@ -559,57 +415,6 @@ func (_c *mockkms_destroyBlob_Call) RunAndReturn(run func(id uint32) error) *moc
559 return _c415 return _c
560 }416 }
561 417
562-// destroyDumb provides a mock function for the type mockkms
563-func (_mock *mockkms) destroyDumb(handle uint32) error {
564- ret := _mock.Called(handle)
565-
566- if len(ret) == 0 {
567- panic("no return value specified for destroyDumb")
568- }
569-
570- var r0 error
571- if returnFunc, ok := ret.Get(0).(func(uint32) error); ok {
572- r0 = returnFunc(handle)
573- } else {
574- r0 = ret.Error(0)
575- }
576- return r0
577-}
578-
579-// mockkms_destroyDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'destroyDumb'
580-type mockkms_destroyDumb_Call struct {
581- *mock.Call
582-}
583-
584-// destroyDumb is a helper method to define mock.On call
585-// - handle uint32
586-func (_e *mockkms_Expecter) destroyDumb(handle any) *mockkms_destroyDumb_Call {
587- return &mockkms_destroyDumb_Call{Call: _e.mock.On("destroyDumb", handle)}
588-}
589-
590-func (_c *mockkms_destroyDumb_Call) Run(run func(handle uint32)) *mockkms_destroyDumb_Call {
591- _c.Call.Run(func(args mock.Arguments) {
592- var arg0 uint32
593- if args[0] != nil {
594- arg0 = args[0].(uint32)
595- }
596- run(
597- arg0,
598- )
599- })
600- return _c
601-}
602-
603-func (_c *mockkms_destroyDumb_Call) Return(err error) *mockkms_destroyDumb_Call {
604- _c.Call.Return(err)
605- return _c
606-}
607-
608-func (_c *mockkms_destroyDumb_Call) RunAndReturn(run func(handle uint32) error) *mockkms_destroyDumb_Call {
609- _c.Call.Return(run)
610- return _c
611-}
612-
613 // getBlob provides a mock function for the type mockkms418 // getBlob provides a mock function for the type mockkms
614 func (_mock *mockkms) getBlob(id uint32) ([]byte, error) {419 func (_mock *mockkms) getBlob(id uint32) ([]byte, error) {
615 ret := _mock.Called(id)420 ret := _mock.Called(id)
@@ -672,68 +477,6 @@ func (_c *mockkms_getBlob_Call) RunAndReturn(run func(id uint32) ([]byte, error)
672 return _c477 return _c
673 }478 }
674 479
675-// mapDumb provides a mock function for the type mockkms
676-func (_mock *mockkms) mapDumb(b dumbBuffer) ([]byte, error) {
677- ret := _mock.Called(b)
678-
679- if len(ret) == 0 {
680- panic("no return value specified for mapDumb")
681- }
682-
683- var r0 []byte
684- var r1 error
685- if returnFunc, ok := ret.Get(0).(func(dumbBuffer) ([]byte, error)); ok {
686- return returnFunc(b)
687- }
688- if returnFunc, ok := ret.Get(0).(func(dumbBuffer) []byte); ok {
689- r0 = returnFunc(b)
690- } else {
691- if ret.Get(0) != nil {
692- r0 = ret.Get(0).([]byte)
693- }
694- }
695- if returnFunc, ok := ret.Get(1).(func(dumbBuffer) error); ok {
696- r1 = returnFunc(b)
697- } else {
698- r1 = ret.Error(1)
699- }
700- return r0, r1
701-}
702-
703-// mockkms_mapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'mapDumb'
704-type mockkms_mapDumb_Call struct {
705- *mock.Call
706-}
707-
708-// mapDumb is a helper method to define mock.On call
709-// - b dumbBuffer
710-func (_e *mockkms_Expecter) mapDumb(b any) *mockkms_mapDumb_Call {
711- return &mockkms_mapDumb_Call{Call: _e.mock.On("mapDumb", b)}
712-}
713-
714-func (_c *mockkms_mapDumb_Call) Run(run func(b dumbBuffer)) *mockkms_mapDumb_Call {
715- _c.Call.Run(func(args mock.Arguments) {
716- var arg0 dumbBuffer
717- if args[0] != nil {
718- arg0 = args[0].(dumbBuffer)
719- }
720- run(
721- arg0,
722- )
723- })
724- return _c
725-}
726-
727-func (_c *mockkms_mapDumb_Call) Return(bytes []byte, err error) *mockkms_mapDumb_Call {
728- _c.Call.Return(bytes, err)
729- return _c
730-}
731-
732-func (_c *mockkms_mapDumb_Call) RunAndReturn(run func(b dumbBuffer) ([]byte, error)) *mockkms_mapDumb_Call {
733- _c.Call.Return(run)
734- return _c
735-}
736-
737 // objProps provides a mock function for the type mockkms480 // objProps provides a mock function for the type mockkms
738 func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) {481 func (_mock *mockkms) objProps(obj uint32, typ uint32) (map[string][2]uint64, error) {
739 ret := _mock.Called(obj, typ)482 ret := _mock.Called(obj, typ)
@@ -1155,54 +898,3 @@ func (_c *mockkms_rmFB_Call) RunAndReturn(run func(id uint32) error) *mockkms_rm
1155 _c.Call.Return(run)898 _c.Call.Return(run)
1156 return _c899 return _c
1157 }900 }
1158-
1159-// unmapDumb provides a mock function for the type mockkms
1160-func (_mock *mockkms) unmapDumb(mem []byte) error {
1161- ret := _mock.Called(mem)
1162-
1163- if len(ret) == 0 {
1164- panic("no return value specified for unmapDumb")
1165- }
1166-
1167- var r0 error
1168- if returnFunc, ok := ret.Get(0).(func([]byte) error); ok {
1169- r0 = returnFunc(mem)
1170- } else {
1171- r0 = ret.Error(0)
1172- }
1173- return r0
1174-}
1175-
1176-// mockkms_unmapDumb_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'unmapDumb'
1177-type mockkms_unmapDumb_Call struct {
1178- *mock.Call
1179-}
1180-
1181-// unmapDumb is a helper method to define mock.On call
1182-// - mem []byte
1183-func (_e *mockkms_Expecter) unmapDumb(mem any) *mockkms_unmapDumb_Call {
1184- return &mockkms_unmapDumb_Call{Call: _e.mock.On("unmapDumb", mem)}
1185-}
1186-
1187-func (_c *mockkms_unmapDumb_Call) Run(run func(mem []byte)) *mockkms_unmapDumb_Call {
1188- _c.Call.Run(func(args mock.Arguments) {
1189- var arg0 []byte
1190- if args[0] != nil {
1191- arg0 = args[0].([]byte)
1192- }
1193- run(
1194- arg0,
1195- )
1196- })
1197- return _c
1198-}
1199-
1200-func (_c *mockkms_unmapDumb_Call) Return(err error) *mockkms_unmapDumb_Call {
1201- _c.Call.Return(err)
1202- return _c
1203-}
1204-
1205-func (_c *mockkms_unmapDumb_Call) RunAndReturn(run func(mem []byte) error) *mockkms_unmapDumb_Call {
1206- _c.Call.Return(run)
1207- return _c
1208-}
M internal/adapters/drm/kms_test.go +0 -3
@@ -32,9 +32,6 @@ func TestStructSizesMatchIoctls(t *testing.T) {
3232 {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic},
3333 {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob},
3434 {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap},
35- {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb},
36- {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb},
37- {"destroy_dumb", unsafe.Sizeof(uint32(0)), ioctlDestroyDumb},
3835 {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob},
3936 } {
4037 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
@@ -32,9 +32,6 @@ func TestStructSizesMatchIoctls(t *testing.T) {
32 {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic},32 {"atomic", unsafe.Sizeof(modeAtomic{}), ioctlAtomic},
33 {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob},33 {"create_blob", unsafe.Sizeof(createBlob{}), ioctlCreateBlob},
34 {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap},34 {"get_cap", unsafe.Sizeof(getCap{}), ioctlGetCap},
35- {"create_dumb", unsafe.Sizeof(createDumb{}), ioctlCreateDumb},
36- {"map_dumb", unsafe.Sizeof(mapDumb{}), ioctlMapDumb},
37- {"destroy_dumb", unsafe.Sizeof(uint32(0)), ioctlDestroyDumb},
38 {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob},35 {"destroy_blob", unsafe.Sizeof(uint32(0)), ioctlDestroyBlob},
39 } {36 } {
40 if want := (tc.req >> 16) & 0x3fff; tc.got != want {37 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
A internal/adapters/drm/nomem_reads_test.go +73 -0
@@ -0,0 +1,73 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "testing"
8+ "testing/synctest"
9+ "time"
10+
11+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12+ "github.com/bnema/neferwl/internal/ports"
13+ "github.com/stretchr/testify/mock"
14+ "golang.org/x/sys/unix"
15+)
16+
17+// A static scene produces no reportDirty wake. Memory retries must reap
18+// completed reads themselves, without dropping an older unsignalled read.
19+func TestNoMemRetriesReapSignalledReads(t *testing.T) {
20+ synctest.Test(t, func(t *testing.T) {
21+ errs := make([]error, 40)
22+ for i := range errs {
23+ errs[i] = unix.ENOMEM
24+ }
25+ o, _, commits := testOutput(t, errs...)
26+ defer o.dropRead()
27+ held, signal := fencePipe(t)
28+ o.holdRead(held)
29+ pending := o.readFences[0]
30+ r := portsmocks.NewMockRenderer(t)
31+ r.EXPECT().UseTarget(0).Return()
32+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
33+ f, w := fencePipe(t)
34+ if _, err := w.Write([]byte{1}); err != nil {
35+ t.Fatal(err)
36+ }
37+ return f, nil
38+ }).Times(len(errs))
39+ enabled := true
40+ var previous *os.File
41+ for range errs {
42+ _, err := o.submitFrame(context.Background(), r, ports.Scene{Seq: 1}, nil, nil, nil, nil)
43+ if !errors.Is(err, unix.ENOMEM) || !o.commitFailed(err, &enabled) {
44+ t.Fatalf("refusal: %v", err)
45+ }
46+ if len(o.readFences) != 2 || o.readFences[0] != pending {
47+ t.Fatalf("retained %d fences; unsignalled read preserved=%v", len(o.readFences), len(o.readFences) > 0 && o.readFences[0] == pending)
48+ }
49+ if previous != nil {
50+ if _, err := previous.Stat(); !errors.Is(err, os.ErrClosed) {
51+ t.Fatalf("completed descriptor not closed: %v", err)
52+ }
53+ }
54+ previous = o.readFences[1]
55+ time.Sleep(o.frame.nomemBackoff)
56+ if o.expire() {
57+ t.Fatal("memory retry requested modeset")
58+ }
59+ }
60+ if len(*commits) != len(errs) {
61+ t.Fatalf("commits %d", len(*commits))
62+ }
63+ if o.readDone() || len(o.readFences) != 1 || o.readFences[0] != pending {
64+ t.Fatal("unsignalled read lost")
65+ }
66+ if _, err := signal.Write([]byte{1}); err != nil {
67+ t.Fatal(err)
68+ }
69+ if !o.readDone() || len(o.readFences) != 0 {
70+ t.Fatal("completed read not reaped")
71+ }
72+ })
73+}
@@ -0,0 +1,73 @@
1+package drm
2+
3+import (
4+ "context"
5+ "errors"
6+ "os"
7+ "testing"
8+ "testing/synctest"
9+ "time"
10+
11+ portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12+ "github.com/bnema/neferwl/internal/ports"
13+ "github.com/stretchr/testify/mock"
14+ "golang.org/x/sys/unix"
15+)
16+
17+// A static scene produces no reportDirty wake. Memory retries must reap
18+// completed reads themselves, without dropping an older unsignalled read.
19+func TestNoMemRetriesReapSignalledReads(t *testing.T) {
20+ synctest.Test(t, func(t *testing.T) {
21+ errs := make([]error, 40)
22+ for i := range errs {
23+ errs[i] = unix.ENOMEM
24+ }
25+ o, _, commits := testOutput(t, errs...)
26+ defer o.dropRead()
27+ held, signal := fencePipe(t)
28+ o.holdRead(held)
29+ pending := o.readFences[0]
30+ r := portsmocks.NewMockRenderer(t)
31+ r.EXPECT().UseTarget(0).Return()
32+ r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
33+ f, w := fencePipe(t)
34+ if _, err := w.Write([]byte{1}); err != nil {
35+ t.Fatal(err)
36+ }
37+ return f, nil
38+ }).Times(len(errs))
39+ enabled := true
40+ var previous *os.File
41+ for range errs {
42+ _, err := o.submitFrame(context.Background(), r, ports.Scene{Seq: 1}, nil, nil, nil, nil)
43+ if !errors.Is(err, unix.ENOMEM) || !o.commitFailed(err, &enabled) {
44+ t.Fatalf("refusal: %v", err)
45+ }
46+ if len(o.readFences) != 2 || o.readFences[0] != pending {
47+ t.Fatalf("retained %d fences; unsignalled read preserved=%v", len(o.readFences), len(o.readFences) > 0 && o.readFences[0] == pending)
48+ }
49+ if previous != nil {
50+ if _, err := previous.Stat(); !errors.Is(err, os.ErrClosed) {
51+ t.Fatalf("completed descriptor not closed: %v", err)
52+ }
53+ }
54+ previous = o.readFences[1]
55+ time.Sleep(o.frame.nomemBackoff)
56+ if o.expire() {
57+ t.Fatal("memory retry requested modeset")
58+ }
59+ }
60+ if len(*commits) != len(errs) {
61+ t.Fatalf("commits %d", len(*commits))
62+ }
63+ if o.readDone() || len(o.readFences) != 1 || o.readFences[0] != pending {
64+ t.Fatal("unsignalled read lost")
65+ }
66+ if _, err := signal.Write([]byte{1}); err != nil {
67+ t.Fatal(err)
68+ }
69+ if !o.readDone() || len(o.readFences) != 0 {
70+ t.Fatal("completed read not reaped")
71+ }
72+ })
73+}
A internal/adapters/drm/nomem_test.go +208 -0
@@ -0,0 +1,208 @@
1+package drm
2+
3+import (
4+ "sync/atomic"
5+ "testing"
6+ "testing/synctest"
7+ "time"
8+
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+ "golang.org/x/sys/unix"
12+)
13+
14+func TestNoMemRetriesWithoutModeset(t *testing.T) {
15+ synctest.Test(t, func(t *testing.T) {
16+ h := startStuck(t, nil, unix.ENOMEM, unix.ENOMEM, nil)
17+ h.scenes <- ports.Scene{Seq: 1}
18+ synctest.Wait()
19+ for _, delay := range []time.Duration{nomemRetryMin, 2 * nomemRetryMin} {
20+ select {
21+ case p := <-h.presented:
22+ t.Fatalf("refused frame reported: %+v", p)
23+ default:
24+ }
25+ time.Sleep(delay)
26+ synctest.Wait()
27+ }
28+ c := h.snapshot()
29+ if len(c) != 5 {
30+ t.Fatalf("commits %d, want 5", len(c))
31+ }
32+ for _, r := range c[2:] {
33+ if r.flags&atomicAllowModes != 0 || r.user&3 != userFrame {
34+ t.Fatalf("not a frame retry: %+v", r)
35+ }
36+ }
37+ select {
38+ case p := <-h.presented:
39+ t.Fatalf("reported before successful flip: %+v", p)
40+ default:
41+ }
42+ h.flipped <- eventOf(c[4])
43+ synctest.Wait()
44+ select {
45+ case <-h.presented:
46+ default:
47+ t.Fatal("successful flip not reported")
48+ }
49+ select {
50+ case p := <-h.presented:
51+ t.Fatalf("extra report: %+v", p)
52+ default:
53+ }
54+ h.stop()
55+ })
56+}
57+
58+func TestLastingNoMemKeepsOutput(t *testing.T) {
59+ synctest.Test(t, func(t *testing.T) {
60+ errs := make([]error, 21)
61+ for i := range errs {
62+ errs[i] = unix.ENOMEM
63+ }
64+ h := startStuck(t, nil, errs...)
65+ h.scenes <- ports.Scene{Seq: 1}
66+ synctest.Wait()
67+ for i := range 19 {
68+ time.Sleep(min(nomemRetryMax, nomemRetryMin*time.Duration(1<<min(i, 7))))
69+ synctest.Wait()
70+ }
71+ select {
72+ case err := <-h.done:
73+ t.Fatalf("Run stopped: %v", err)
74+ default:
75+ }
76+ c := h.stop()
77+ if len(c) != 22 {
78+ t.Fatalf("commits %d want 22", len(c))
79+ }
80+ for _, r := range c[2:] {
81+ if r.flags&atomicAllowModes != 0 {
82+ t.Fatal("ENOMEM triggered modeset")
83+ }
84+ }
85+ })
86+}
87+
88+func TestNoMemModesetRetries(t *testing.T) {
89+ synctest.Test(t, func(t *testing.T) {
90+ h := startStuck(t, nil, nil, unix.ENOMEM, nil)
91+ h.scenes <- ports.Scene{Seq: 1, Off: true}
92+ synctest.Wait()
93+ if h.count() != 3 {
94+ t.Fatal("power off did not commit")
95+ }
96+ h.scenes <- ports.Scene{Seq: 2}
97+ synctest.Wait()
98+ time.Sleep(nomemRetryMin)
99+ synctest.Wait()
100+ select {
101+ case err := <-h.done:
102+ t.Fatalf("Run stopped: %v", err)
103+ default:
104+ }
105+ c := h.stop()
106+ if len(c) != 6 {
107+ t.Fatalf("commits %d want 6", len(c))
108+ }
109+ for _, r := range c[3:5] {
110+ if r.flags != atomicAllowModes {
111+ t.Fatalf("not a retried modeset: %#x", r.flags)
112+ }
113+ }
114+ })
115+}
116+
117+func TestScanoutNoMemComposes(t *testing.T) {
118+ synctest.Test(t, func(t *testing.T) {
119+ o, _, _ := testOutput(t, unix.ENOMEM)
120+ s, surfaces := fullscreenScene()
121+ c := surfaces[1]
122+ o.clientFBs[9] = &clientFB{fbID: 80}
123+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
124+ direct, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorBypass)
125+ if direct || err != nil || o.reason != "nomem" || o.clientFBs[9].failed != "" {
126+ t.Fatalf("direct=%v err=%v reason=%q failed=%q", direct, err, o.reason, o.clientFBs[9].failed)
127+ }
128+ if d := o.composeFrame(s, surfaces); d.overlay.fb != 0 || d.composed.Seq != s.Seq {
129+ t.Fatal("immediate fallback did not compose")
130+ }
131+ for _, delay := range []time.Duration{0, nomemScanoutPause - time.Nanosecond} {
132+ time.Sleep(delay)
133+ d := o.decideFrame(s, surfaces, false)
134+ if d.fb != 0 || d.overlay.fb != 0 || d.composed.Seq != s.Seq || o.reason != "nomem" {
135+ t.Fatalf("not composed: %+v reason=%q", d, o.reason)
136+ }
137+ }
138+ time.Sleep(time.Nanosecond)
139+ if d := o.decideFrame(s, surfaces, false); d.fb != 80 {
140+ t.Fatalf("scanout not resumed: %+v reason=%q", d, o.reason)
141+ }
142+ })
143+}
144+
145+func TestProtectedNoMemKeepsProtectionRetry(t *testing.T) {
146+ for _, stage := range []string{"modeset", "commit"} {
147+ t.Run(stage, func(t *testing.T) {
148+ o, k, _ := testOutput(t, unix.ENOMEM)
149+ var state atomic.Uint64
150+ state.Store(3)
151+ securityGate(t, o, &state)
152+ o.observeSecurity()
153+ events := make(chan ports.SecurityBackendEvent, 4)
154+ o.SecurityEvents = events
155+ var err error
156+ if stage == "modeset" {
157+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
158+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
159+ err = o.modesetImage(70, true)
160+ } else {
161+ o.securityPrepared = true // an admitted lock-screen frame
162+ err = o.commitFrame(70, nil, false, false, pendingFrame{security: o.securityState})
163+ }
164+ enabled := true
165+ if !o.commitFailed(err, &enabled) || !enabled || o.securityPrepared || !o.protectBackoffActive() || !o.frame.nomemSince.IsZero() {
166+ t.Fatalf("protection retry bypassed: err=%v prepared=%v backoff=%v nomem=%v", err, o.securityPrepared, o.protectBackoff, o.frame.nomemSince)
167+ }
168+ select {
169+ case ev := <-events:
170+ t.Fatalf("proof on refusal: %T", ev)
171+ default:
172+ }
173+ })
174+ }
175+}
176+
177+func TestNoMemSuccessClearsRun(t *testing.T) {
178+ o, _, _ := testOutput(t, unix.ENOMEM, nil)
179+ enabled := true
180+ if !o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) {
181+ t.Fatal("ENOMEM unhandled")
182+ }
183+ o.frame.timeout(o.frame.stuckAt, true)
184+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
185+ t.Fatal(err)
186+ }
187+ if !o.frame.nomemSince.IsZero() || o.frame.nomemAttempts != 0 {
188+ t.Fatal("successful commit did not clear run")
189+ }
190+}
191+
192+func TestNoMemRetryRechecksSecurity(t *testing.T) {
193+ o, _, commits := testOutput(t, unix.ENOMEM)
194+ enabled := true
195+ if !o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) {
196+ t.Fatal("ENOMEM unhandled")
197+ }
198+ o.frame.timeout(o.frame.stuckAt, true)
199+ var state atomic.Uint64
200+ state.Store(3)
201+ securityGate(t, o, &state)
202+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != errSecurityScene {
203+ t.Fatalf("stale retry: %v", err)
204+ }
205+ if len(*commits) != 1 {
206+ t.Fatal("stale retry reached KMS")
207+ }
208+}
@@ -0,0 +1,208 @@
1+package drm
2+
3+import (
4+ "sync/atomic"
5+ "testing"
6+ "testing/synctest"
7+ "time"
8+
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+ "golang.org/x/sys/unix"
12+)
13+
14+func TestNoMemRetriesWithoutModeset(t *testing.T) {
15+ synctest.Test(t, func(t *testing.T) {
16+ h := startStuck(t, nil, unix.ENOMEM, unix.ENOMEM, nil)
17+ h.scenes <- ports.Scene{Seq: 1}
18+ synctest.Wait()
19+ for _, delay := range []time.Duration{nomemRetryMin, 2 * nomemRetryMin} {
20+ select {
21+ case p := <-h.presented:
22+ t.Fatalf("refused frame reported: %+v", p)
23+ default:
24+ }
25+ time.Sleep(delay)
26+ synctest.Wait()
27+ }
28+ c := h.snapshot()
29+ if len(c) != 5 {
30+ t.Fatalf("commits %d, want 5", len(c))
31+ }
32+ for _, r := range c[2:] {
33+ if r.flags&atomicAllowModes != 0 || r.user&3 != userFrame {
34+ t.Fatalf("not a frame retry: %+v", r)
35+ }
36+ }
37+ select {
38+ case p := <-h.presented:
39+ t.Fatalf("reported before successful flip: %+v", p)
40+ default:
41+ }
42+ h.flipped <- eventOf(c[4])
43+ synctest.Wait()
44+ select {
45+ case <-h.presented:
46+ default:
47+ t.Fatal("successful flip not reported")
48+ }
49+ select {
50+ case p := <-h.presented:
51+ t.Fatalf("extra report: %+v", p)
52+ default:
53+ }
54+ h.stop()
55+ })
56+}
57+
58+func TestLastingNoMemKeepsOutput(t *testing.T) {
59+ synctest.Test(t, func(t *testing.T) {
60+ errs := make([]error, 21)
61+ for i := range errs {
62+ errs[i] = unix.ENOMEM
63+ }
64+ h := startStuck(t, nil, errs...)
65+ h.scenes <- ports.Scene{Seq: 1}
66+ synctest.Wait()
67+ for i := range 19 {
68+ time.Sleep(min(nomemRetryMax, nomemRetryMin*time.Duration(1<<min(i, 7))))
69+ synctest.Wait()
70+ }
71+ select {
72+ case err := <-h.done:
73+ t.Fatalf("Run stopped: %v", err)
74+ default:
75+ }
76+ c := h.stop()
77+ if len(c) != 22 {
78+ t.Fatalf("commits %d want 22", len(c))
79+ }
80+ for _, r := range c[2:] {
81+ if r.flags&atomicAllowModes != 0 {
82+ t.Fatal("ENOMEM triggered modeset")
83+ }
84+ }
85+ })
86+}
87+
88+func TestNoMemModesetRetries(t *testing.T) {
89+ synctest.Test(t, func(t *testing.T) {
90+ h := startStuck(t, nil, nil, unix.ENOMEM, nil)
91+ h.scenes <- ports.Scene{Seq: 1, Off: true}
92+ synctest.Wait()
93+ if h.count() != 3 {
94+ t.Fatal("power off did not commit")
95+ }
96+ h.scenes <- ports.Scene{Seq: 2}
97+ synctest.Wait()
98+ time.Sleep(nomemRetryMin)
99+ synctest.Wait()
100+ select {
101+ case err := <-h.done:
102+ t.Fatalf("Run stopped: %v", err)
103+ default:
104+ }
105+ c := h.stop()
106+ if len(c) != 6 {
107+ t.Fatalf("commits %d want 6", len(c))
108+ }
109+ for _, r := range c[3:5] {
110+ if r.flags != atomicAllowModes {
111+ t.Fatalf("not a retried modeset: %#x", r.flags)
112+ }
113+ }
114+ })
115+}
116+
117+func TestScanoutNoMemComposes(t *testing.T) {
118+ synctest.Test(t, func(t *testing.T) {
119+ o, _, _ := testOutput(t, unix.ENOMEM)
120+ s, surfaces := fullscreenScene()
121+ c := surfaces[1]
122+ o.clientFBs[9] = &clientFB{fbID: 80}
123+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
124+ direct, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorBypass)
125+ if direct || err != nil || o.reason != "nomem" || o.clientFBs[9].failed != "" {
126+ t.Fatalf("direct=%v err=%v reason=%q failed=%q", direct, err, o.reason, o.clientFBs[9].failed)
127+ }
128+ if d := o.composeFrame(s, surfaces); d.overlay.fb != 0 || d.composed.Seq != s.Seq {
129+ t.Fatal("immediate fallback did not compose")
130+ }
131+ for _, delay := range []time.Duration{0, nomemScanoutPause - time.Nanosecond} {
132+ time.Sleep(delay)
133+ d := o.decideFrame(s, surfaces, false)
134+ if d.fb != 0 || d.overlay.fb != 0 || d.composed.Seq != s.Seq || o.reason != "nomem" {
135+ t.Fatalf("not composed: %+v reason=%q", d, o.reason)
136+ }
137+ }
138+ time.Sleep(time.Nanosecond)
139+ if d := o.decideFrame(s, surfaces, false); d.fb != 80 {
140+ t.Fatalf("scanout not resumed: %+v reason=%q", d, o.reason)
141+ }
142+ })
143+}
144+
145+func TestProtectedNoMemKeepsProtectionRetry(t *testing.T) {
146+ for _, stage := range []string{"modeset", "commit"} {
147+ t.Run(stage, func(t *testing.T) {
148+ o, k, _ := testOutput(t, unix.ENOMEM)
149+ var state atomic.Uint64
150+ state.Store(3)
151+ securityGate(t, o, &state)
152+ o.observeSecurity()
153+ events := make(chan ports.SecurityBackendEvent, 4)
154+ o.SecurityEvents = events
155+ var err error
156+ if stage == "modeset" {
157+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
158+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
159+ err = o.modesetImage(70, true)
160+ } else {
161+ o.securityPrepared = true // an admitted lock-screen frame
162+ err = o.commitFrame(70, nil, false, false, pendingFrame{security: o.securityState})
163+ }
164+ enabled := true
165+ if !o.commitFailed(err, &enabled) || !enabled || o.securityPrepared || !o.protectBackoffActive() || !o.frame.nomemSince.IsZero() {
166+ t.Fatalf("protection retry bypassed: err=%v prepared=%v backoff=%v nomem=%v", err, o.securityPrepared, o.protectBackoff, o.frame.nomemSince)
167+ }
168+ select {
169+ case ev := <-events:
170+ t.Fatalf("proof on refusal: %T", ev)
171+ default:
172+ }
173+ })
174+ }
175+}
176+
177+func TestNoMemSuccessClearsRun(t *testing.T) {
178+ o, _, _ := testOutput(t, unix.ENOMEM, nil)
179+ enabled := true
180+ if !o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) {
181+ t.Fatal("ENOMEM unhandled")
182+ }
183+ o.frame.timeout(o.frame.stuckAt, true)
184+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
185+ t.Fatal(err)
186+ }
187+ if !o.frame.nomemSince.IsZero() || o.frame.nomemAttempts != 0 {
188+ t.Fatal("successful commit did not clear run")
189+ }
190+}
191+
192+func TestNoMemRetryRechecksSecurity(t *testing.T) {
193+ o, _, commits := testOutput(t, unix.ENOMEM)
194+ enabled := true
195+ if !o.commitFailed(o.commitFrame(70, nil, false, false, pendingFrame{}), &enabled) {
196+ t.Fatal("ENOMEM unhandled")
197+ }
198+ o.frame.timeout(o.frame.stuckAt, true)
199+ var state atomic.Uint64
200+ state.Store(3)
201+ securityGate(t, o, &state)
202+ if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != errSecurityScene {
203+ t.Fatalf("stale retry: %v", err)
204+ }
205+ if len(*commits) != 1 {
206+ t.Fatal("stale retry reached KMS")
207+ }
208+}
M internal/adapters/drm/output.go +83 -67
@@ -109,12 +109,14 @@ type Output struct {
109109 readFences []*os.File
110110 // Direct scanout: client framebuffers by DMABuf ID and the buffer on
111111 // screen and queued (0: the composed image).
112- scanout bool
113- clientFBs map[uint64]*clientFB
114- shown, queued uint64
115- planeRect planeRect
116- reason string // why the last frame was composed ("" = scanout)
117- reports presented.Queue
112+ scanout bool
113+ clientFBs map[uint64]*clientFB
114+ shown, queued uint64
115+ planeRect planeRect
116+ reason string // why the last frame was composed ("" = scanout)
117+ scanoutPausedUntil time.Time
118+ nomemModeset bool // retry the refused modeset, not an ordinary frame
119+ reports presented.Queue
118120 // Last immutable shows snapshot sent in a flip. Never mutate it: wayland
119121 // may still be reading a prior report on another goroutine.
120122 shows surfaces.ShowsSnapshot
@@ -415,6 +417,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
415417 if err := o.disableProtected(); err != nil {
416418 return err
417419 }
420+ o.commitRecovered()
421+ o.nomemModeset = false
418422 o.frame.resetAfterModeset()
419423 o.forgetColor()
420424 o.planeRect = fullPlaneRect(o.Width(), o.Height())
@@ -450,6 +454,9 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
450454 return errSecurityScene
451455 }
452456 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
457+ if !o.protected && errors.Is(err, unix.ENOMEM) {
458+ o.nomemModeset = true
459+ }
453460 _ = o.k.destroyBlob(blob)
454461 if o.protected {
455462 return protectedCommitError{fmt.Errorf("modeset: %w", err)}
@@ -458,6 +465,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
458465 }
459466 // Only now is nothing of ours pending or on screen: on failure the
460467 // previous buffers may still show.
468+ o.commitRecovered()
469+ o.nomemModeset = false
461470 o.frame.resetAfterModeset()
462471 // Plane scaling and colour pipeline capabilities may change after a
463472 // modeset or VT resume; the planes' colour state is not assumed.
@@ -514,6 +523,8 @@ func (o *Output) powerOff() error {
514523 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
515524 return fmt.Errorf("power off: %w", err)
516525 }
526+ o.commitRecovered()
527+ o.nomemModeset = false
517528 o.colorBypassed()
518529 o.off, o.vrrGame = true, false
519530 // No vblank to aim at: the modeset that turns it on commits the state.
@@ -579,44 +590,18 @@ func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) {
579590 return blob, nil
580591 }
581592
582-// testCursor asks KMS whether the cursor plane takes the cursor images.
583-// Refused dumb images are replaced by the renderer's; when those are refused
584-// too, or there are none, the hardware cursor goes off. It reports whether
585-// the cursor changed (new images or off).
586-func (o *Output) testCursor() (changed bool) {
593+// testCursor turns the hardware cursor off when KMS refuses its images on
594+// the cursor plane (e.g. a pitch the plane cannot scan out).
595+func (o *Output) testCursor() {
587596 c := o.cursor
588597 if c == nil || c.off || c.fbs[0] == 0 {
589- return false
590- }
591- for {
592- req := &atomicReq{}
593- c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
594- err := o.k.commit(req, atomicTestOnly, 0)
595- if !refused(err) {
596- return changed
597- }
598- // Dumb images refused: the renderer's get one try.
599- if c.alloc != allocDumb {
600- o.cursorOff(err)
601- return true
602- }
603- ok, ferr := c.useFallback(o.cursorNits())
604- if !ok || ferr != nil {
605- o.cursorOff(errors.Join(err, ferr))
606- return true
607- }
608- changed = true
609- o.log.Warn().Err(err).Str("connector", o.conn.name).Str("alloc", allocVulkan).Int("size", c.size).Msg("cursor plane refused dumb images; hardware cursor uses renderer images")
598+ return
610599 }
611-}
612-
613-// cursorNits is the SDR white the cursor images are converted for on an HDR
614-// output (0: SDR).
615-func (o *Output) cursorNits() float64 {
616- if !o.hdr.on {
617- return 0
600+ req := &atomicReq{}
601+ c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
602+ if err := o.k.commit(req, atomicTestOnly, 0); refused(err) {
603+ o.cursorOff(err)
618604 }
619- return float64(o.hdr.settings.SDRBrightness)
620605 }
621606
622607 // cursorOff stops using the cursor plane for the rest of the output's life.
@@ -761,6 +746,10 @@ func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f p
761746 return nil
762747 }
763748 }
749+ if o.protected && errors.Is(err, unix.ENOMEM) {
750+ o.invalidateSecurity()
751+ return protectedCommitError{err}
752+ }
764753 return err
765754 }
766755 if !async {
@@ -793,6 +782,7 @@ func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f p
793782 f.fenceReady = signalled(f.fences)
794783 }
795784 o.setAsync(async)
785+ o.commitRecovered()
796786 o.frame.begin(f, time.Now())
797787 return nil
798788 }
@@ -823,10 +813,11 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState,
823813 }
824814
825815 // cursorRefused reports, after a commit carrying the cursor failed with
826-// EINVAL, whether the cursor plane was the cause; then the cursor is off or
827-// on other images, and the commit can be retried.
816+// EINVAL, whether the cursor plane was the cause; then the cursor is off
817+// and the commit can be retried without it.
828818 func (o *Output) cursorRefused() bool {
829- return o.testCursor()
819+ o.testCursor()
820+ return o.cursor.off
830821 }
831822
832823 // commitState commits a cursor or VRR change without a new frame.
@@ -909,6 +900,7 @@ func (o *Output) commitState(vrr bool) error {
909900 o.setVRR(vrr)
910901 o.contentValue = o.contentWanted
911902 o.flipStats.stateCommits++
903+ o.commitRecovered()
912904 o.frame.begin(pendingFrame{}, time.Now())
913905 return nil
914906 }
@@ -962,6 +954,12 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF
962954 cfb.noAsync = true
963955 err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
964956 }
957+ if errors.Is(err, unix.ENOMEM) && !retryableProtected(err) {
958+ o.scanoutPausedUntil = time.Now().Add(nomemScanoutPause)
959+ o.setScanoutReason("nomem")
960+ o.renderLog.Info().Err(err).Str("connector", o.conn.name).Msg("scanout pin failed; composing")
961+ return false, nil
962+ }
965963 if errors.Is(err, errColorRefused) {
966964 // KMS takes the buffer without the pipeline: compose it; the
967965 // refusal is cached, the buffer is not marked failed.
@@ -1067,7 +1065,7 @@ func (o *Output) Close() {
10671065 o.ctmBlob = 0
10681066 }
10691067 if o.cursor != nil {
1070- o.cursor.free()
1068+ o.cursor.free(o.k)
10711069 }
10721070 o.freeImages()
10731071 if o.alloc != nil {
@@ -1111,15 +1109,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
11111109 }
11121110 defer r.Close()
11131111 if o.cursor != nil {
1114- dumbErr, err := o.cursor.setup(o.k, r)
1115- switch {
1116- case err != nil:
1112+ if err := o.cursor.setup(o.k, r); err != nil {
11171113 o.cursor.off = true
1118- o.log.Warn().Err(errors.Join(dumbErr, err)).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
1119- case dumbErr != nil:
1120- o.log.Warn().Err(dumbErr).Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor")
1121- default:
1122- o.log.Info().Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor")
1114+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
11231115 }
11241116 }
11251117 // The seat sends its state first; while switched away (e.g. a monitor
@@ -1133,13 +1125,13 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
11331125 // While switched away they are made on the first enable.
11341126 if enabled {
11351127 if err := o.showImages(r, imagesDriver, nil); err != nil {
1136- // While protected, a recoverable KMS failure keeps the output
1137- // registered and dark; the loop retries with bounded backoff.
1138- if !o.protected || !o.commitFailed(err, &enabled) {
1128+ // Protected preparation and a memory-refused modeset keep the
1129+ // output registered; the loop retries with capped backoff.
1130+ if !o.protected && !o.nomemModeset || !o.commitFailed(err, &enabled) {
11391131 return err
11401132 }
11411133 }
1142- if !o.protected {
1134+ if !o.protected && !o.frame.pendingCommit() {
11431135 o.probeAsync(r)
11441136 }
11451137 }
@@ -1494,7 +1486,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
14941486 stateDirty = true
14951487 if o.cursor != nil && !o.protected {
14961488 // An image that waited for a free slot loads now.
1497- if _, err := o.cursor.flushLater(o.cursorNits()); err != nil {
1489+ if _, err := o.cursor.flushLater(r); err != nil {
14981490 o.log.Warn().Err(err).Msg("cursor")
14991491 }
15001492 }
@@ -1515,7 +1507,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
15151507 }
15161508 if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) {
15171509 cursorScale, cursorTransform = s.Scale, s.Transform
1518- o.setCursor(loadCursor, want, s.Scale, s.Transform)
1510+ o.setCursor(r, loadCursor, want, s.Scale, s.Transform)
15191511 stateDirty = true
15201512 }
15211513 scene, haveScene, dirty = s, true, true
@@ -1530,7 +1522,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
15301522 want = c
15311523 // Before the first scene the scale is unknown: loaded then.
15321524 if o.cursor != nil && loadCursor != nil && cursorScale > 0 {
1533- o.setCursor(loadCursor, want, cursorScale, cursorTransform)
1525+ o.setCursor(r, loadCursor, want, cursorScale, cursorTransform)
15341526 stateDirty = true
15351527 }
15361528 case b := <-contents:
@@ -1588,7 +1580,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
15881580 }
15891581 // Output power: off waits for no commit in flight; on is a modeset
15901582 // and a full frame.
1591- if o.wantOff != o.off {
1583+ if o.wantOff != o.off || o.nomemModeset {
15921584 var err error
15931585 if o.wantOff {
15941586 err = o.powerOff()
@@ -1598,8 +1590,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
15981590 o.probeAsync(r) // skipped while it started off
15991591 }
16001592 }
1601- if err != nil && !o.commitFailed(err, &enabled) {
1602- return err
1593+ if err != nil {
1594+ if !o.commitFailed(err, &enabled) {
1595+ return err
1596+ }
1597+ continue
16031598 }
16041599 }
16051600 if o.off {
@@ -1665,9 +1660,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
16651660 }
16661661 }
16671662
1668-// commitFailed handles a commit error that does not stop the output: a
1669-// commit still in flight (EBUSY, retried after its event) or DRM master
1670-// lost mid-switch (the seat disable follows).
1663+// commitFailed handles refusals that keep the output: busy or memory
1664+// pressure (retried), protected preparation, or DRM master lost mid-switch
1665+// (the seat disable follows).
16711666 func (o *Output) commitFailed(err error, enabled *bool) bool {
16721667 switch {
16731668 case errors.Is(err, errSecurityScene), errors.Is(err, errProtectionPending):
@@ -1688,6 +1683,20 @@ func (o *Output) commitFailed(err error, enabled *bool) bool {
16881683 o.deferProtection()
16891684 o.log.Warn().Err(err).Str("connector", o.conn.name).Dur("retry_in", o.protectBackoff).Msg("protected commit refused; output stays protected, retrying")
16901685 return true
1686+ case errors.Is(err, unix.ENOMEM):
1687+ now := time.Now()
1688+ previous := o.frame.lastNomem
1689+ attempt, newRun := o.frame.nomem(now)
1690+ age := now.Sub(o.frame.nomemSince)
1691+ switch {
1692+ case newRun:
1693+ o.log.Warn().Err(err).Str("connector", o.conn.name).Dur("retry_in", o.frame.nomemBackoff).Msg("commit refused for memory; keeping last frame, retrying")
1694+ case previous.Sub(o.frame.nomemSince) < nomemWarnAfter && age >= nomemWarnAfter:
1695+ o.log.Warn().Str("connector", o.conn.name).Int("attempts", attempt).Dur("age", age).Msg("commit still refused for memory; keeping last frame, retrying")
1696+ default:
1697+ o.log.Debug().Str("connector", o.conn.name).Int("attempts", attempt).Dur("age", age).Dur("retry_in", o.frame.nomemBackoff).Msg("commit refused for memory")
1698+ }
1699+ return true
16911700 case lostMaster(err):
16921701 o.observeSecurity()
16931702 if o.protected {
@@ -1701,6 +1710,13 @@ func (o *Output) commitFailed(err error, enabled *bool) bool {
17011710 return false
17021711 }
17031712
1713+// commitRecovered reports only an accepted commit, never a refused frame.
1714+func (o *Output) commitRecovered() {
1715+ if attempts, outage := o.frame.nomemRecovered(time.Now()); attempts > 0 {
1716+ o.log.Info().Str("connector", o.conn.name).Int("attempts", attempts).Dur("outage", outage).Msg("commit recovered")
1717+ }
1718+}
1719+
17041720 // flipDone times the completion ev of the commit f made at start: the
17051721 // flip stats, the flip trace and the VRR flip gap. It never allocates
17061722 // with tracing off.
@@ -1767,14 +1783,14 @@ func (o *Output) wakeCore() { corewake.Send(o.Frames, o.conn.name) }
17671783
17681784 // setCursor loads a cursor for scale and output transform t into the cursor
17691785 // images.
1770-func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) {
1786+func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) {
17711787 o.observeSecurity()
17721788 if o.protected || o.cursor.off {
17731789 return
17741790 }
17751791 img, err := load(c, scale, t, o.cursor.Limit())
17761792 if err == nil {
1777- err = o.cursor.setImage(img.Pixels, img.W, img.H, img.HotX, img.HotY, o.cursorNits())
1793+ err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY)
17781794 }
17791795 if err != nil {
17801796 o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor")
@@ -109,12 +109,14 @@ type Output struct {
109 readFences []*os.File109 readFences []*os.File
110 // Direct scanout: client framebuffers by DMABuf ID and the buffer on110 // Direct scanout: client framebuffers by DMABuf ID and the buffer on
111 // screen and queued (0: the composed image).111 // screen and queued (0: the composed image).
112- scanout bool112+ scanout bool
113- clientFBs map[uint64]*clientFB113+ clientFBs map[uint64]*clientFB
114- shown, queued uint64114+ shown, queued uint64
115- planeRect planeRect115+ planeRect planeRect
116- reason string // why the last frame was composed ("" = scanout)116+ reason string // why the last frame was composed ("" = scanout)
117- reports presented.Queue117+ scanoutPausedUntil time.Time
118+ nomemModeset bool // retry the refused modeset, not an ordinary frame
119+ reports presented.Queue
118 // Last immutable shows snapshot sent in a flip. Never mutate it: wayland120 // Last immutable shows snapshot sent in a flip. Never mutate it: wayland
119 // may still be reading a prior report on another goroutine.121 // may still be reading a prior report on another goroutine.
120 shows surfaces.ShowsSnapshot122 shows surfaces.ShowsSnapshot
@@ -415,6 +417,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
415 if err := o.disableProtected(); err != nil {417 if err := o.disableProtected(); err != nil {
416 return err418 return err
417 }419 }
420+ o.commitRecovered()
421+ o.nomemModeset = false
418 o.frame.resetAfterModeset()422 o.frame.resetAfterModeset()
419 o.forgetColor()423 o.forgetColor()
420 o.planeRect = fullPlaneRect(o.Width(), o.Height())424 o.planeRect = fullPlaneRect(o.Width(), o.Height())
@@ -450,6 +454,9 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
450 return errSecurityScene454 return errSecurityScene
451 }455 }
452 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {456 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
457+ if !o.protected && errors.Is(err, unix.ENOMEM) {
458+ o.nomemModeset = true
459+ }
453 _ = o.k.destroyBlob(blob)460 _ = o.k.destroyBlob(blob)
454 if o.protected {461 if o.protected {
455 return protectedCommitError{fmt.Errorf("modeset: %w", err)}462 return protectedCommitError{fmt.Errorf("modeset: %w", err)}
@@ -458,6 +465,8 @@ func (o *Output) modesetImage(fb uint32, active bool) error {
458 }465 }
459 // Only now is nothing of ours pending or on screen: on failure the466 // Only now is nothing of ours pending or on screen: on failure the
460 // previous buffers may still show.467 // previous buffers may still show.
468+ o.commitRecovered()
469+ o.nomemModeset = false
461 o.frame.resetAfterModeset()470 o.frame.resetAfterModeset()
462 // Plane scaling and colour pipeline capabilities may change after a471 // Plane scaling and colour pipeline capabilities may change after a
463 // modeset or VT resume; the planes' colour state is not assumed.472 // modeset or VT resume; the planes' colour state is not assumed.
@@ -514,6 +523,8 @@ func (o *Output) powerOff() error {
514 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {523 if err := o.k.commit(req, atomicAllowModes, 0); err != nil {
515 return fmt.Errorf("power off: %w", err)524 return fmt.Errorf("power off: %w", err)
516 }525 }
526+ o.commitRecovered()
527+ o.nomemModeset = false
517 o.colorBypassed()528 o.colorBypassed()
518 o.off, o.vrrGame = true, false529 o.off, o.vrrGame = true, false
519 // No vblank to aim at: the modeset that turns it on commits the state.530 // No vblank to aim at: the modeset that turns it on commits the state.
@@ -579,44 +590,18 @@ func (o *Output) modeBlobFor(mode modeInfo) (uint32, error) {
579 return blob, nil590 return blob, nil
580 }591 }
581 592
582-// testCursor asks KMS whether the cursor plane takes the cursor images.593+// testCursor turns the hardware cursor off when KMS refuses its images on
583-// Refused dumb images are replaced by the renderer's; when those are refused594+// the cursor plane (e.g. a pitch the plane cannot scan out).
584-// too, or there are none, the hardware cursor goes off. It reports whether595+func (o *Output) testCursor() {
585-// the cursor changed (new images or off).
586-func (o *Output) testCursor() (changed bool) {
587 c := o.cursor596 c := o.cursor
588 if c == nil || c.off || c.fbs[0] == 0 {597 if c == nil || c.off || c.fbs[0] == 0 {
589- return false598+ return
590- }
591- for {
592- req := &atomicReq{}
593- c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
594- err := o.k.commit(req, atomicTestOnly, 0)
595- if !refused(err) {
596- return changed
597- }
598- // Dumb images refused: the renderer's get one try.
599- if c.alloc != allocDumb {
600- o.cursorOff(err)
601- return true
602- }
603- ok, ferr := c.useFallback(o.cursorNits())
604- if !ok || ferr != nil {
605- o.cursorOff(errors.Join(err, ferr))
606- return true
607- }
608- changed = true
609- o.log.Warn().Err(err).Str("connector", o.conn.name).Str("alloc", allocVulkan).Int("size", c.size).Msg("cursor plane refused dumb images; hardware cursor uses renderer images")
610 }599 }
611-}600+ req := &atomicReq{}
612-601+ c.props(req, o.crtc, cursorState{on: true, fb: c.fbs[0]})
613-// cursorNits is the SDR white the cursor images are converted for on an HDR602+ if err := o.k.commit(req, atomicTestOnly, 0); refused(err) {
614-// output (0: SDR).603+ o.cursorOff(err)
615-func (o *Output) cursorNits() float64 {
616- if !o.hdr.on {
617- return 0
618 }604 }
619- return float64(o.hdr.settings.SDRBrightness)
620 }605 }
621 606
622 // cursorOff stops using the cursor plane for the rest of the output's life.607 // cursorOff stops using the cursor plane for the rest of the output's life.
@@ -761,6 +746,10 @@ func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f p
761 return nil746 return nil
762 }747 }
763 }748 }
749+ if o.protected && errors.Is(err, unix.ENOMEM) {
750+ o.invalidateSecurity()
751+ return protectedCommitError{err}
752+ }
764 return err753 return err
765 }754 }
766 if !async {755 if !async {
@@ -793,6 +782,7 @@ func (o *Output) commitRect(fb uint32, fence *os.File, async bool, vrr bool, f p
793 f.fenceReady = signalled(f.fences)782 f.fenceReady = signalled(f.fences)
794 }783 }
795 o.setAsync(async)784 o.setAsync(async)
785+ o.commitRecovered()
796 o.frame.begin(f, time.Now())786 o.frame.begin(f, time.Now())
797 return nil787 return nil
798 }788 }
@@ -823,10 +813,11 @@ func (o *Output) frameTest(fb uint32, fence *os.File, vrr bool, cur cursorState,
823 }813 }
824 814
825 // cursorRefused reports, after a commit carrying the cursor failed with815 // cursorRefused reports, after a commit carrying the cursor failed with
826-// EINVAL, whether the cursor plane was the cause; then the cursor is off or816+// EINVAL, whether the cursor plane was the cause; then the cursor is off
827-// on other images, and the commit can be retried.817+// and the commit can be retried without it.
828 func (o *Output) cursorRefused() bool {818 func (o *Output) cursorRefused() bool {
829- return o.testCursor()819+ o.testCursor()
820+ return o.cursor.off
830 }821 }
831 822
832 // commitState commits a cursor or VRR change without a new frame.823 // commitState commits a cursor or VRR change without a new frame.
@@ -909,6 +900,7 @@ func (o *Output) commitState(vrr bool) error {
909 o.setVRR(vrr)900 o.setVRR(vrr)
910 o.contentValue = o.contentWanted901 o.contentValue = o.contentWanted
911 o.flipStats.stateCommits++902 o.flipStats.stateCommits++
903+ o.commitRecovered()
912 o.frame.begin(pendingFrame{}, time.Now())904 o.frame.begin(pendingFrame{}, time.Now())
913 return nil905 return nil
914 }906 }
@@ -962,6 +954,12 @@ func (o *Output) commitScanoutRect(fb uint32, c ports.SurfaceContent, f pendingF
962 cfb.noAsync = true954 cfb.noAsync = true
963 err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)955 err = o.commitRect(fb, fence, false, vrr, f, overlayWin{}, rect, pc)
964 }956 }
957+ if errors.Is(err, unix.ENOMEM) && !retryableProtected(err) {
958+ o.scanoutPausedUntil = time.Now().Add(nomemScanoutPause)
959+ o.setScanoutReason("nomem")
960+ o.renderLog.Info().Err(err).Str("connector", o.conn.name).Msg("scanout pin failed; composing")
961+ return false, nil
962+ }
965 if errors.Is(err, errColorRefused) {963 if errors.Is(err, errColorRefused) {
966 // KMS takes the buffer without the pipeline: compose it; the964 // KMS takes the buffer without the pipeline: compose it; the
967 // refusal is cached, the buffer is not marked failed.965 // refusal is cached, the buffer is not marked failed.
@@ -1067,7 +1065,7 @@ func (o *Output) Close() {
1067 o.ctmBlob = 01065 o.ctmBlob = 0
1068 }1066 }
1069 if o.cursor != nil {1067 if o.cursor != nil {
1070- o.cursor.free()1068+ o.cursor.free(o.k)
1071 }1069 }
1072 o.freeImages()1070 o.freeImages()
1073 if o.alloc != nil {1071 if o.alloc != nil {
@@ -1111,15 +1109,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1111 }1109 }
1112 defer r.Close()1110 defer r.Close()
1113 if o.cursor != nil {1111 if o.cursor != nil {
1114- dumbErr, err := o.cursor.setup(o.k, r)1112+ if err := o.cursor.setup(o.k, r); err != nil {
1115- switch {
1116- case err != nil:
1117 o.cursor.off = true1113 o.cursor.off = true
1118- o.log.Warn().Err(errors.Join(dumbErr, err)).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")1114+ o.log.Warn().Err(err).Str("connector", o.conn.name).Msg("no hardware cursor images; hardware cursor off")
1119- case dumbErr != nil:
1120- o.log.Warn().Err(dumbErr).Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor")
1121- default:
1122- o.log.Info().Str("connector", o.conn.name).Str("alloc", o.cursor.alloc).Int("size", o.cursor.size).Msg("hardware cursor")
1123 }1115 }
1124 }1116 }
1125 // The seat sends its state first; while switched away (e.g. a monitor1117 // The seat sends its state first; while switched away (e.g. a monitor
@@ -1133,13 +1125,13 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1133 // While switched away they are made on the first enable.1125 // While switched away they are made on the first enable.
1134 if enabled {1126 if enabled {
1135 if err := o.showImages(r, imagesDriver, nil); err != nil {1127 if err := o.showImages(r, imagesDriver, nil); err != nil {
1136- // While protected, a recoverable KMS failure keeps the output1128+ // Protected preparation and a memory-refused modeset keep the
1137- // registered and dark; the loop retries with bounded backoff.1129+ // output registered; the loop retries with capped backoff.
1138- if !o.protected || !o.commitFailed(err, &enabled) {1130+ if !o.protected && !o.nomemModeset || !o.commitFailed(err, &enabled) {
1139 return err1131 return err
1140 }1132 }
1141 }1133 }
1142- if !o.protected {1134+ if !o.protected && !o.frame.pendingCommit() {
1143 o.probeAsync(r)1135 o.probeAsync(r)
1144 }1136 }
1145 }1137 }
@@ -1494,7 +1486,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1494 stateDirty = true1486 stateDirty = true
1495 if o.cursor != nil && !o.protected {1487 if o.cursor != nil && !o.protected {
1496 // An image that waited for a free slot loads now.1488 // An image that waited for a free slot loads now.
1497- if _, err := o.cursor.flushLater(o.cursorNits()); err != nil {1489+ if _, err := o.cursor.flushLater(r); err != nil {
1498 o.log.Warn().Err(err).Msg("cursor")1490 o.log.Warn().Err(err).Msg("cursor")
1499 }1491 }
1500 }1492 }
@@ -1515,7 +1507,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1515 }1507 }
1516 if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) {1508 if !o.protected && o.cursor != nil && loadCursor != nil && (s.Scale != cursorScale || s.Transform != cursorTransform) {
1517 cursorScale, cursorTransform = s.Scale, s.Transform1509 cursorScale, cursorTransform = s.Scale, s.Transform
1518- o.setCursor(loadCursor, want, s.Scale, s.Transform)1510+ o.setCursor(r, loadCursor, want, s.Scale, s.Transform)
1519 stateDirty = true1511 stateDirty = true
1520 }1512 }
1521 scene, haveScene, dirty = s, true, true1513 scene, haveScene, dirty = s, true, true
@@ -1530,7 +1522,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1530 want = c1522 want = c
1531 // Before the first scene the scale is unknown: loaded then.1523 // Before the first scene the scale is unknown: loaded then.
1532 if o.cursor != nil && loadCursor != nil && cursorScale > 0 {1524 if o.cursor != nil && loadCursor != nil && cursorScale > 0 {
1533- o.setCursor(loadCursor, want, cursorScale, cursorTransform)1525+ o.setCursor(r, loadCursor, want, cursorScale, cursorTransform)
1534 stateDirty = true1526 stateDirty = true
1535 }1527 }
1536 case b := <-contents:1528 case b := <-contents:
@@ -1588,7 +1580,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1588 }1580 }
1589 // Output power: off waits for no commit in flight; on is a modeset1581 // Output power: off waits for no commit in flight; on is a modeset
1590 // and a full frame.1582 // and a full frame.
1591- if o.wantOff != o.off {1583+ if o.wantOff != o.off || o.nomemModeset {
1592 var err error1584 var err error
1593 if o.wantOff {1585 if o.wantOff {
1594 err = o.powerOff()1586 err = o.powerOff()
@@ -1598,8 +1590,11 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1598 o.probeAsync(r) // skipped while it started off1590 o.probeAsync(r) // skipped while it started off
1599 }1591 }
1600 }1592 }
1601- if err != nil && !o.commitFailed(err, &enabled) {1593+ if err != nil {
1602- return err1594+ if !o.commitFailed(err, &enabled) {
1595+ return err
1596+ }
1597+ continue
1603 }1598 }
1604 }1599 }
1605 if o.off {1600 if o.off {
@@ -1665,9 +1660,9 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
1665 }1660 }
1666 }1661 }
1667 1662
1668-// commitFailed handles a commit error that does not stop the output: a1663+// commitFailed handles refusals that keep the output: busy or memory
1669-// commit still in flight (EBUSY, retried after its event) or DRM master1664+// pressure (retried), protected preparation, or DRM master lost mid-switch
1670-// lost mid-switch (the seat disable follows).1665+// (the seat disable follows).
1671 func (o *Output) commitFailed(err error, enabled *bool) bool {1666 func (o *Output) commitFailed(err error, enabled *bool) bool {
1672 switch {1667 switch {
1673 case errors.Is(err, errSecurityScene), errors.Is(err, errProtectionPending):1668 case errors.Is(err, errSecurityScene), errors.Is(err, errProtectionPending):
@@ -1688,6 +1683,20 @@ func (o *Output) commitFailed(err error, enabled *bool) bool {
1688 o.deferProtection()1683 o.deferProtection()
1689 o.log.Warn().Err(err).Str("connector", o.conn.name).Dur("retry_in", o.protectBackoff).Msg("protected commit refused; output stays protected, retrying")1684 o.log.Warn().Err(err).Str("connector", o.conn.name).Dur("retry_in", o.protectBackoff).Msg("protected commit refused; output stays protected, retrying")
1690 return true1685 return true
1686+ case errors.Is(err, unix.ENOMEM):
1687+ now := time.Now()
1688+ previous := o.frame.lastNomem
1689+ attempt, newRun := o.frame.nomem(now)
1690+ age := now.Sub(o.frame.nomemSince)
1691+ switch {
1692+ case newRun:
1693+ o.log.Warn().Err(err).Str("connector", o.conn.name).Dur("retry_in", o.frame.nomemBackoff).Msg("commit refused for memory; keeping last frame, retrying")
1694+ case previous.Sub(o.frame.nomemSince) < nomemWarnAfter && age >= nomemWarnAfter:
1695+ o.log.Warn().Str("connector", o.conn.name).Int("attempts", attempt).Dur("age", age).Msg("commit still refused for memory; keeping last frame, retrying")
1696+ default:
1697+ o.log.Debug().Str("connector", o.conn.name).Int("attempts", attempt).Dur("age", age).Dur("retry_in", o.frame.nomemBackoff).Msg("commit refused for memory")
1698+ }
1699+ return true
1691 case lostMaster(err):1700 case lostMaster(err):
1692 o.observeSecurity()1701 o.observeSecurity()
1693 if o.protected {1702 if o.protected {
@@ -1701,6 +1710,13 @@ func (o *Output) commitFailed(err error, enabled *bool) bool {
1701 return false1710 return false
1702 }1711 }
1703 1712
1713+// commitRecovered reports only an accepted commit, never a refused frame.
1714+func (o *Output) commitRecovered() {
1715+ if attempts, outage := o.frame.nomemRecovered(time.Now()); attempts > 0 {
1716+ o.log.Info().Str("connector", o.conn.name).Int("attempts", attempts).Dur("outage", outage).Msg("commit recovered")
1717+ }
1718+}
1719+
1704 // flipDone times the completion ev of the commit f made at start: the1720 // flipDone times the completion ev of the commit f made at start: the
1705 // flip stats, the flip trace and the VRR flip gap. It never allocates1721 // flip stats, the flip trace and the VRR flip gap. It never allocates
1706 // with tracing off.1722 // with tracing off.
@@ -1767,14 +1783,14 @@ func (o *Output) wakeCore() { corewake.Send(o.Frames, o.conn.name) }
1767 1783
1768 // setCursor loads a cursor for scale and output transform t into the cursor1784 // setCursor loads a cursor for scale and output transform t into the cursor
1769 // images.1785 // images.
1770-func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) {1786+func (o *Output) setCursor(r ports.Renderer, load CursorLoader, c ports.CursorChange, scale float64, t ports.BufferTransform) {
1771 o.observeSecurity()1787 o.observeSecurity()
1772 if o.protected || o.cursor.off {1788 if o.protected || o.cursor.off {
1773 return1789 return
1774 }1790 }
1775 img, err := load(c, scale, t, o.cursor.Limit())1791 img, err := load(c, scale, t, o.cursor.Limit())
1776 if err == nil {1792 if err == nil {
1777- err = o.cursor.setImage(img.Pixels, img.W, img.H, img.HotX, img.HotY, o.cursorNits())1793+ err = o.cursor.setImage(r, img.Pixels, img.W, img.H, img.HotX, img.HotY)
1778 }1794 }
1779 if err != nil {1795 if err != nil {
1780 o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor")1796 o.log.Warn().Err(err).Float64("scale", scale).Int("transform", int(t)).Msg("cursor")
M internal/adapters/drm/reads.go +3 -1
@@ -41,8 +41,10 @@ func signalled(fs []*os.File) bool {
4141 return true
4242 }
4343
44-// holdRead keeps the fences of a frame that was not committed.
44+// holdRead keeps the fences of a frame that was not committed. Reap old
45+// completed reads even when no content publication asks for a report.
4546 func (o *Output) holdRead(fs ...*os.File) {
47+ o.readDone()
4648 o.readFences = append(o.readFences, dupFences(fs...)...)
4749 }
4850
@@ -41,8 +41,10 @@ func signalled(fs []*os.File) bool {
41 return true41 return true
42 }42 }
43 43
44-// holdRead keeps the fences of a frame that was not committed.44+// holdRead keeps the fences of a frame that was not committed. Reap old
45+// completed reads even when no content publication asks for a report.
45 func (o *Output) holdRead(fs ...*os.File) {46 func (o *Output) holdRead(fs ...*os.File) {
47+ o.readDone()
46 o.readFences = append(o.readFences, dupFences(fs...)...)48 o.readFences = append(o.readFences, dupFences(fs...)...)
47 }49 }
48 50
M internal/adapters/drm/scanout.go +10 -0
@@ -335,6 +335,11 @@ type frameDecision struct {
335335 // composeFrame chooses overlay or composition after direct scanout cannot commit.
336336 // The caller owns the returned overlay fence.
337337 func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) frameDecision {
338+ if time.Now().Before(o.scanoutPausedUntil) {
339+ o.setScanoutReason("nomem")
340+ o.setOverlayReason("nomem")
341+ return frameDecision{composed: s}
342+ }
338343 ov, rest := o.overlayFrame(s, surfaces)
339344 if ov.fb != 0 {
340345 return frameDecision{overlay: ov, composed: rest}
@@ -342,6 +347,8 @@ func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S
342347 return frameDecision{composed: s}
343348 }
344349
350+const nomemScanoutPause = 2 * time.Second
351+
345352 // decideFrame tries direct scanout, then overlay, then composition.
346353 // Captures require composition; the caller owns the returned overlay fence.
347354 func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, capture bool) frameDecision {
@@ -350,6 +357,9 @@ func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.Su
350357 o.setOverlayReason("capture")
351358 return frameDecision{composed: s}
352359 }
360+ if time.Now().Before(o.scanoutPausedUntil) {
361+ return o.composeFrame(s, surfaces)
362+ }
353363 fb, c, mode := o.scanoutFrame(s, surfaces)
354364 if fb != 0 {
355365 return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height()), color: mode}
@@ -335,6 +335,11 @@ type frameDecision struct {
335 // composeFrame chooses overlay or composition after direct scanout cannot commit.335 // composeFrame chooses overlay or composition after direct scanout cannot commit.
336 // The caller owns the returned overlay fence.336 // The caller owns the returned overlay fence.
337 func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) frameDecision {337 func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) frameDecision {
338+ if time.Now().Before(o.scanoutPausedUntil) {
339+ o.setScanoutReason("nomem")
340+ o.setOverlayReason("nomem")
341+ return frameDecision{composed: s}
342+ }
338 ov, rest := o.overlayFrame(s, surfaces)343 ov, rest := o.overlayFrame(s, surfaces)
339 if ov.fb != 0 {344 if ov.fb != 0 {
340 return frameDecision{overlay: ov, composed: rest}345 return frameDecision{overlay: ov, composed: rest}
@@ -342,6 +347,8 @@ func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.S
342 return frameDecision{composed: s}347 return frameDecision{composed: s}
343 }348 }
344 349
350+const nomemScanoutPause = 2 * time.Second
351+
345 // decideFrame tries direct scanout, then overlay, then composition.352 // decideFrame tries direct scanout, then overlay, then composition.
346 // Captures require composition; the caller owns the returned overlay fence.353 // Captures require composition; the caller owns the returned overlay fence.
347 func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, capture bool) frameDecision {354 func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, capture bool) frameDecision {
@@ -350,6 +357,9 @@ func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.Su
350 o.setOverlayReason("capture")357 o.setOverlayReason("capture")
351 return frameDecision{composed: s}358 return frameDecision{composed: s}
352 }359 }
360+ if time.Now().Before(o.scanoutPausedUntil) {
361+ return o.composeFrame(s, surfaces)
362+ }
353 fb, c, mode := o.scanoutFrame(s, surfaces)363 fb, c, mode := o.scanoutFrame(s, surfaces)
354 if fb != 0 {364 if fb != 0 {
355 return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height()), color: mode}365 return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height()), color: mode}
M internal/adapters/drm/stuck_test.go +13 -1
@@ -33,6 +33,9 @@ type stuckRun struct {
3333 contents *contentFeed
3434 active chan bool
3535 presented chan ports.OutputPresented
36+ flipped chan flipEvent
37+ done <-chan error
38+ snapshot func() []commitRec
3639 sink ports.PresentationSink
3740 count func() int
3841 stop func() []commitRec
@@ -46,7 +49,8 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
4649 o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...)
4750 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
4851 o.frame.stuckAfter = 200 * time.Millisecond
49- o.flipped = make(chan flipEvent) // no event ever
52+ flipped := make(chan flipEvent)
53+ o.flipped = flipped // tests send events explicitly
5054 r := portsmocks.NewMockRenderer(t)
5155 buf := func() ports.DMABuf {
5256 f, w, _ := os.Pipe()
@@ -64,6 +68,8 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
6468 return render(), nil
6569 })
6670 r.EXPECT().Close().Return().Once()
71+ r.EXPECT().Trim(mock.Anything).Return(nil).Maybe()
72+ r.EXPECT().TakeRedrawn().Return(0).Maybe()
6773 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
6874 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
6975 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
@@ -72,6 +78,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
7278 h.sink, h.presented = presentedSink(t, 64)
7379 ctx, cancel := context.WithCancel(context.Background())
7480 done := make(chan error, 1)
81+ h.done, h.flipped = done, flipped
82+ h.snapshot = func() []commitRec {
83+ commitMu.Lock()
84+ defer commitMu.Unlock()
85+ return append([]commitRec(nil), (*commits)...)
86+ }
7587 go func() {
7688 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents.in(), h.contents.ret, nil, h.sink, nil, nil)
7789 }()
@@ -33,6 +33,9 @@ type stuckRun struct {
33 contents *contentFeed33 contents *contentFeed
34 active chan bool34 active chan bool
35 presented chan ports.OutputPresented35 presented chan ports.OutputPresented
36+ flipped chan flipEvent
37+ done <-chan error
38+ snapshot func() []commitRec
36 sink ports.PresentationSink39 sink ports.PresentationSink
37 count func() int40 count func() int
38 stop func() []commitRec41 stop func() []commitRec
@@ -46,7 +49,8 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
46 o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...)49 o, k, commits, commitMu := testOutputMu(t, append([]error{nil, nil}, errs...)...)
47 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}50 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
48 o.frame.stuckAfter = 200 * time.Millisecond51 o.frame.stuckAfter = 200 * time.Millisecond
49- o.flipped = make(chan flipEvent) // no event ever52+ flipped := make(chan flipEvent)
53+ o.flipped = flipped // tests send events explicitly
50 r := portsmocks.NewMockRenderer(t)54 r := portsmocks.NewMockRenderer(t)
51 buf := func() ports.DMABuf {55 buf := func() ports.DMABuf {
52 f, w, _ := os.Pipe()56 f, w, _ := os.Pipe()
@@ -64,6 +68,8 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
64 return render(), nil68 return render(), nil
65 })69 })
66 r.EXPECT().Close().Return().Once()70 r.EXPECT().Close().Return().Once()
71+ r.EXPECT().Trim(mock.Anything).Return(nil).Maybe()
72+ r.EXPECT().TakeRedrawn().Return(0).Maybe()
67 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()73 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
68 k.EXPECT().createBlob(mock.Anything).Return(99, nil)74 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
69 k.EXPECT().destroyBlob(mock.Anything).Return(nil)75 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
@@ -72,6 +78,12 @@ func startStuck(t *testing.T, render func() *os.File, errs ...error) *stuckRun {
72 h.sink, h.presented = presentedSink(t, 64)78 h.sink, h.presented = presentedSink(t, 64)
73 ctx, cancel := context.WithCancel(context.Background())79 ctx, cancel := context.WithCancel(context.Background())
74 done := make(chan error, 1)80 done := make(chan error, 1)
81+ h.done, h.flipped = done, flipped
82+ h.snapshot = func() []commitRec {
83+ commitMu.Lock()
84+ defer commitMu.Unlock()
85+ return append([]commitRec(nil), (*commits)...)
86+ }
75 go func() {87 go func() {
76 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents.in(), h.contents.ret, nil, h.sink, nil, nil)88 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, h.active, h.scenes, h.contents.in(), h.contents.ret, nil, h.sink, nil, nil)
77 }()89 }()
D internal/adapters/hdrcursor/convert_test.go +0 -29
@@ -1,29 +0,0 @@
1-package hdrcursor
2-
3-import (
4- "bytes"
5- "testing"
6-)
7-
8-func TestCacheKeyGeometry(t *testing.T) {
9- px := []byte{10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255}
10- for _, tc := range []struct {
11- name string
12- w, h, side int
13- }{{"dimensions", 1, 4, 4}, {"max-side", 2, 2, 1}} {
14- t.Run(tc.name, func(t *testing.T) {
15- var c Cache
16- c.Convert(px, 2, 2, 4, 203)
17- before := c.Key()
18- got := bytes.Clone(c.Convert(px, tc.w, tc.h, tc.side, 203))
19- if c.Key() == before {
20- t.Fatal("geometry missing from cache key")
21- }
22- var fresh Cache
23- want := fresh.Convert(px, tc.w, tc.h, tc.side, 203)
24- if !bytes.Equal(got, want) {
25- t.Fatalf("stale conversion: %v want %v", got, want)
26- }
27- })
28- }
29-}
@@ -1,29 +0,0 @@
1-package hdrcursor
2-
3-import (
4- "bytes"
5- "testing"
6-)
7-
8-func TestCacheKeyGeometry(t *testing.T) {
9- px := []byte{10, 20, 30, 255, 40, 50, 60, 255, 70, 80, 90, 255, 100, 110, 120, 255}
10- for _, tc := range []struct {
11- name string
12- w, h, side int
13- }{{"dimensions", 1, 4, 4}, {"max-side", 2, 2, 1}} {
14- t.Run(tc.name, func(t *testing.T) {
15- var c Cache
16- c.Convert(px, 2, 2, 4, 203)
17- before := c.Key()
18- got := bytes.Clone(c.Convert(px, tc.w, tc.h, tc.side, 203))
19- if c.Key() == before {
20- t.Fatal("geometry missing from cache key")
21- }
22- var fresh Cache
23- want := fresh.Convert(px, tc.w, tc.h, tc.side, 203)
24- if !bytes.Equal(got, want) {
25- t.Fatalf("stale conversion: %v want %v", got, want)
26- }
27- })
28- }
29-}
D internal/adapters/hdrcursor/hdrcursor.go +0 -100
D internal/adapters/hdrcursor/hdrcursor_test.go +0 -76
M internal/adapters/vulkan/cursor.go +42 -5
M internal/adapters/vulkan/cursor_test.go +25 -6
M internal/adapters/vulkan/hdr.go +2 -2
M internal/adapters/vulkan/pq.go +34 -6
M internal/adapters/vulkan/pq_test.go +5 -0
M internal/adapters/vulkan/renderer.go +1 -2
M packaging/arch/PKGBUILD +1 -1
M packaging/aur/neferwl-git/PKGBUILD +1 -1

Comments1 open / 1

bnema

Field check on the target machine (RX 9070 XT, two outputs) while a PyTorch training job ran for 30 min: 10 commits refused with ENOMEM (HDMI-A-1: 2, DP-2: 8). Every refusal recovered on the first retry (attempts: 1) after 19 to 80 ms; both outputs stayed on. Before this change each refusal stopped its output.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close