Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions negpy/desktop/view/canvas/gpu_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ def _upload_display_lut(self) -> None:
address_mode_w=wgpu.AddressMode.clamp_to_edge,
)

def presents_to_screen(self) -> bool:
"""True when the frame goes to a native surface of its own, which the overlay
must punch an alpha hole to reveal.

rendercanvas picks a *bitmap* present on Qt: the frame is blitted into the
shared backing store by the widget's own paintEvent, and a hole punched over
it erases the frame instead of revealing anything.
"""
sub = getattr(self.canvas, "_subwidget", self.canvas)
return bool(getattr(sub, "_present_to_screen", False))

def set_transform(self, zoom: float, px: float, py: float) -> None:
self.zoom = zoom
self.pan_x = px
Expand Down
13 changes: 9 additions & 4 deletions negpy/desktop/view/canvas/overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,15 +571,20 @@ def paintEvent(self, event) -> None:
painter = QPainter(self)

parent_bg = getattr(self.parent(), "_bg_color", QColor("#050505"))
if not getattr(self.parent(), "gpu_widget", None) or not self.parent().gpu_widget.isVisible():
gpu = getattr(self.parent(), "gpu_widget", None)
gpu_live = bool(gpu is not None and gpu.isVisible())
if not gpu_live:
painter.fillRect(event.rect(), parent_bg)

if sys.platform in ("darwin", "win32"):
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
if getattr(self.parent(), "gpu_widget", None) and self.parent().gpu_widget.isVisible():
painter.fillRect(event.rect(), Qt.GlobalColor.transparent)
else:
if not gpu_live:
painter.fillRect(event.rect(), parent_bg)
elif gpu.presents_to_screen():
# Only a native surface underneath is revealed by the hole. Under a
# bitmap present the frame is in this very backing store, and the
# fill wipes it (issue: white canvas on macOS, black on Windows).
painter.fillRect(event.rect(), Qt.GlobalColor.transparent)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)

painter.setRenderHint(QPainter.RenderHint.Antialiasing)
Expand Down
93 changes: 93 additions & 0 deletions tests/test_overlay_gpu_compositing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""The overlay must not erase the frame the GPU canvas painted underneath it.

rendercanvas presents through a bitmap on Qt: the canvas blits its frame into the
shared backing store. The alpha hole the overlay punches on macOS/Windows was written
for a native surface underneath; over a bitmap present it wipes the frame, and the
canvas shows the bare window background (white on macOS, black on Windows).
"""

import sys

import pytest
from PyQt6.QtGui import QColor, QImage
from PyQt6.QtWidgets import QWidget

from negpy.desktop.session import AppState
from negpy.desktop.view.canvas.overlay import CanvasOverlay

W, H = 60, 40
FRAME = QColor(20, 160, 90)


class _FakeGPUWidget:
def __init__(self, visible: bool, to_screen: bool):
self._visible, self._to_screen = visible, to_screen

def isVisible(self) -> bool:
return self._visible

def presents_to_screen(self) -> bool:
return self._to_screen


class _FakeCanvas(QWidget):
"""Stands in for ImageCanvas: owns the background colour and the GPU widget."""

def __init__(self, gpu):
super().__init__()
self._bg_color = QColor("#050505")
self.gpu_widget = gpu


def _paint_over_frame(gpu) -> QImage:
"""Paint the overlay onto a surface that already holds a rendered frame."""
canvas = _FakeCanvas(gpu)
overlay = CanvasOverlay(AppState(), canvas)
overlay.resize(W, H)

surface = QImage(W, H, QImage.Format.Format_ARGB32_Premultiplied)
surface.fill(FRAME)
overlay.render(surface)
return surface


@pytest.mark.skipif(sys.platform not in ("darwin", "win32"), reason="the alpha hole is macOS/Windows only")
def test_bitmap_present_frame_survives_the_overlay():
surface = _paint_over_frame(_FakeGPUWidget(visible=True, to_screen=False))

assert QColor(surface.pixel(W // 2, H // 2)) == FRAME


@pytest.mark.skipif(sys.platform not in ("darwin", "win32"), reason="the alpha hole is macOS/Windows only")
def test_screen_present_still_gets_its_hole():
surface = _paint_over_frame(_FakeGPUWidget(visible=True, to_screen=True))

assert QColor.fromRgba(surface.pixel(W // 2, H // 2)).alpha() == 0


def test_hidden_gpu_widget_gets_the_canvas_background():
surface = _paint_over_frame(_FakeGPUWidget(visible=False, to_screen=False))

assert QColor(surface.pixel(W // 2, H // 2)) == QColor("#050505")


def test_presents_to_screen_reads_the_canvas_present_method():
from negpy.desktop.view.canvas.gpu_widget import GPUCanvasWidget

widget = GPUCanvasWidget.__new__(GPUCanvasWidget)

class _Sub:
_present_to_screen = False

class _Canvas:
_subwidget = _Sub()

widget.canvas = _Canvas()
assert widget.presents_to_screen() is False

_Sub._present_to_screen = True
assert widget.presents_to_screen() is True

# Before get_context() the method is unresolved; no hole until it is known.
_Sub._present_to_screen = None
assert widget.presents_to_screen() is False
Loading