From 02fea5769978fe8c4038fc143e6e03ce834defd3 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Thu, 20 Aug 2026 16:02:27 -0400 Subject: [PATCH 1/4] Pass the configured view_formats through to the present texture `configure()` accepts `view_formats`, validates each against the context capabilities, and stores them in the config -- and then the texture is created without them, so a view in any of those formats fails and the argument silently does nothing. The case this matters for is a canvas configured with an srgb format whose contents also need to be read as raw bytes: sampling an srgb view applies the transfer function, so anything reading the frame back sees values that differ from what was written. A view in the matching non-srgb format is the supported way to ask for the unconverted bytes, and it requires the format to be declared when the texture is created. The surface-backed path already forwards these to the surface configuration; this brings the bitmap path in line. --- rendercanvas/contexts/wgpucontext.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rendercanvas/contexts/wgpucontext.py b/rendercanvas/contexts/wgpucontext.py index 4a34fe9..646716a 100644 --- a/rendercanvas/contexts/wgpucontext.py +++ b/rendercanvas/contexts/wgpucontext.py @@ -330,6 +330,7 @@ def _get_current_texture(self): size=need_texture_size, format=self._config["format"], usage=self._config["usage"] | self._context_texture_usage, + view_formats=self._config["view_formats"], ) return self._texture From 9b2cdb35029f2fb329a56f1d2eea51a30f3887cf Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sat, 22 Aug 2026 17:33:24 -0400 Subject: [PATCH 2/4] Test that configured view_formats reach the present texture Covers the plumbing in both directions: with a format declared the view is created, and without it the same view is rejected, so the positive assertion is testing this change rather than something the backend would have allowed anyway. Skipped when the wgpu backend does not implement create_texture(view_formats=...), which it did not before pygfx/wgpu-py#832 -- otherwise the test would fail for a reason unrelated to rendercanvas. --- tests/test_context.py | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_context.py b/tests/test_context.py index 8a3c07e..3d8b14b 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -296,5 +296,62 @@ def test_wgpu_context_hdr(): assert np.all(result * 255 == bitmap) +def _create_texture_supports_view_formats(device): + """Whether the wgpu backend implements create_texture(view_formats=...). + + It raised NotImplementedError until pygfx/wgpu-py#832, so the test below + would fail for a reason that has nothing to do with rendercanvas. + """ + import wgpu + + try: + device.create_texture( + size=(4, 4, 1), + format=wgpu.TextureFormat.rgba8unorm_srgb, + usage=wgpu.TextureUsage.RENDER_ATTACHMENT, + view_formats=[wgpu.TextureFormat.rgba8unorm], + ) + except NotImplementedError: + return False + return True + + +@pytest.mark.skipif(not can_use_wgpu_lib, reason="Needs wgpu lib") +def test_wgpu_context_view_formats(): + # A format passed to configure() must reach the present texture, or a view + # in that format cannot be created and the argument silently does nothing. + import wgpu + + device = wgpu.utils.get_default_device() + if not _create_texture_supports_view_formats(device): + pytest.skip("wgpu backend does not implement create_texture(view_formats)") + + usage = wgpu.TextureUsage.RENDER_ATTACHMENT | wgpu.TextureUsage.TEXTURE_BINDING + + canvas = ManualOffscreenRenderCanvas() + context = canvas.get_context("wgpu") + context.configure( + device=device, + format=wgpu.TextureFormat.rgba8unorm_srgb, + usage=usage, + view_formats=[wgpu.TextureFormat.rgba8unorm], + ) + texture = context.get_current_texture() + assert texture.format == wgpu.TextureFormat.rgba8unorm_srgb + # The declared format is viewable: this is what the parameter is for. + assert texture.create_view(format=wgpu.TextureFormat.rgba8unorm) is not None + + # Without it, the same view is rejected -- so the assertion above is + # testing the plumbing rather than something the backend allows anyway. + canvas2 = ManualOffscreenRenderCanvas() + context2 = canvas2.get_context("wgpu") + context2.configure( + device=device, format=wgpu.TextureFormat.rgba8unorm_srgb, usage=usage + ) + texture2 = context2.get_current_texture() + with pytest.raises(Exception): + texture2.create_view(format=wgpu.TextureFormat.rgba8unorm) + + if __name__ == "__main__": run_tests(globals()) From 03d18cb48ff35dda0b91b7357b043946322a9735 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sat, 22 Aug 2026 17:56:07 -0400 Subject: [PATCH 3/4] Assert the specific error for an undeclared view format Ruff's B017 rejects a blind `pytest.raises(Exception)`, and rightly so here: the negative half of the test is only evidence that the format has to be declared if it fails on the validation of that format, and not on some unrelated error in the same call. Name the error wgpu raises and match its message. --- tests/test_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_context.py b/tests/test_context.py index 3d8b14b..679ae25 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -349,7 +349,7 @@ def test_wgpu_context_view_formats(): device=device, format=wgpu.TextureFormat.rgba8unorm_srgb, usage=usage ) texture2 = context2.get_current_texture() - with pytest.raises(Exception): + with pytest.raises(wgpu.GPUValidationError, match="view format"): texture2.create_view(format=wgpu.TextureFormat.rgba8unorm) From bbdd6bbd634557441bfb2547410af80141a03f1b Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sat, 22 Aug 2026 18:17:40 -0400 Subject: [PATCH 4/4] Fail instead of skip while wgpu lacks view_formats
Claude's draft The test feature-detected `create_texture(view_formats=...)` and skipped when the backend did not implement it, which is every released wgpu: the argument raises NotImplementedError until pygfx/wgpu-py#832 ships. So CI skipped it every run, the PR looked green, and nothing about the pass-through was verified. A skipped test is not evidence. Drop the feature detection and let the test run. On a wgpu without the argument, `get_current_texture()` raises NotImplementedError and the test turns that into an explicit failure naming the version in use and the wgpu-py PR it is waiting on, rather than an opaque traceback. On a wgpu with it, the test passes, and no marker has to be removed by hand for that to happen. The failure state is the point: this PR depends on something unreleased, and red is the honest report of that until the release lands. Verified in both worlds. Against wgpu 0.32 the test fails with the message above; against a wgpu that implements the argument it passes. Removing the `view_formats=` pass-through from `WgpuContextToBitmap._get_current_texture()` while keeping the implementing wgpu also fails it (GPUValidationError on the view), so the test cannot go green without the change it covers. Resume this Claude session: ``` cd /home/mark/git/ramona/python-owl claude --resume 66360ec9-8ab8-4ae1-8f3f-e873ce8fc563 ```
Claude-Session: https://claude.ai/code/session_01FvMxuoA4rU1KvqM38ypUwe --- tests/test_context.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index 679ae25..3063d1d 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -296,26 +296,6 @@ def test_wgpu_context_hdr(): assert np.all(result * 255 == bitmap) -def _create_texture_supports_view_formats(device): - """Whether the wgpu backend implements create_texture(view_formats=...). - - It raised NotImplementedError until pygfx/wgpu-py#832, so the test below - would fail for a reason that has nothing to do with rendercanvas. - """ - import wgpu - - try: - device.create_texture( - size=(4, 4, 1), - format=wgpu.TextureFormat.rgba8unorm_srgb, - usage=wgpu.TextureUsage.RENDER_ATTACHMENT, - view_formats=[wgpu.TextureFormat.rgba8unorm], - ) - except NotImplementedError: - return False - return True - - @pytest.mark.skipif(not can_use_wgpu_lib, reason="Needs wgpu lib") def test_wgpu_context_view_formats(): # A format passed to configure() must reach the present texture, or a view @@ -323,9 +303,6 @@ def test_wgpu_context_view_formats(): import wgpu device = wgpu.utils.get_default_device() - if not _create_texture_supports_view_formats(device): - pytest.skip("wgpu backend does not implement create_texture(view_formats)") - usage = wgpu.TextureUsage.RENDER_ATTACHMENT | wgpu.TextureUsage.TEXTURE_BINDING canvas = ManualOffscreenRenderCanvas() @@ -336,7 +313,19 @@ def test_wgpu_context_view_formats(): usage=usage, view_formats=[wgpu.TextureFormat.rgba8unorm], ) - texture = context.get_current_texture() + + try: + texture = context.get_current_texture() + except NotImplementedError: + # create_texture() itself refuses view_formats: that is pygfx/wgpu-py#832, + # which is not in a wgpu release yet. Fail rather than skip -- a skip + # would leave this pass-through unverified while CI stayed green, and + # this test is the thing that tells us when the release lands. + pytest.fail( + f"wgpu {wgpu.__version__} does not implement create_texture(view_formats=..)," + " which this needs; see pygfx/wgpu-py#832" + ) + assert texture.format == wgpu.TextureFormat.rgba8unorm_srgb # The declared format is viewable: this is what the parameter is for. assert texture.create_view(format=wgpu.TextureFormat.rgba8unorm) is not None