native_sdk_gtk_present_gpu_surface_pixels receives the dirty rectangle the runtime computed and opens by throwing it away, then converts the entire surface from straight RGBA8 to premultiplied ARGB32 one pixel at a time. At 1400x900 that is 1.26 million pixels per frame, nearly all of them recomputing bytes that already hold the correct value.
src/platform/linux/gtk_host.c, the first six lines of the function:
int native_sdk_gtk_present_gpu_surface_pixels(..., double scale, int has_dirty_rect, double dirty_x, double dirty_y, double dirty_width, double dirty_height, const uint8_t *rgba8, size_t rgba8_len) {
(void)scale;
(void)has_dirty_rect;
(void)dirty_x;
(void)dirty_y;
(void)dirty_width;
(void)dirty_height;
and then, unconditionally:
for (size_t row = 0; row < height; row++) {
...
for (size_t col = 0; col < width; col++) {
/* premultiply, 4 multiplies and a divide per pixel */
}
}
Why the information is already correct
The incoming buffer is always the full surface (the function returns 0 unless rgba8_len == width * height * 4), and the reference rasteriser only re-inks its scissor rect: renderPass computes the scissor, clears only that rect on a .load, and culls each command against it. So everything outside the dirty rect is already right in both the source buffer and the cairo buffer. Nothing needs recomputing.
Windows already does this
native_sdk_windows_present_gpu_surface_pixels in src/platform/windows/webview2_host.cpp has the exact shape, presumably from #236: a same_buffer guard, finite checks on scale and the four doubles, normalise for negative extents, floor/ceil outward so a partly covered pixel is converted, clamp to the surface. Porting it to the GTK host is a direct translation, with cairo's premultiply in place of GDI's opaque BGRA and one extra guard: convert in full when the cairo buffer was just reallocated, since it holds nothing.
I have been running that port and it behaves. Happy to open a PR if you take them, or to paste the diff here.
One thing that cannot be fixed the same way
The repaint has to stay whole-widget. GTK 4 removed partial invalidation, so gtk_widget_queue_draw_area is a GTK 3 API and there is no way to ask for less than the widget. Only the conversion narrows. Handing GTK a GdkTexture or a dmabuf instead of a pixel buffer would be the larger fix, but that is a different piece of work and the conversion loop is worth removing on its own.
Why it matters beyond the microseconds
The published frame numbers for apps built on this are macOS numbers, because macOS is the only platform that registers a packet presenter, so ui_app.zig's gate can never be taken on Linux. That is fine and expected. What is not obvious is that the Linux path then also discards the incremental work the runtime did anyway, so the gap is wider than "no GPU" accounts for. I would rather the software path be honestly incremental before anyone measures it.
native_sdk_gtk_present_gpu_surface_pixelsreceives the dirty rectangle the runtime computed and opens by throwing it away, then converts the entire surface from straight RGBA8 to premultiplied ARGB32 one pixel at a time. At 1400x900 that is 1.26 million pixels per frame, nearly all of them recomputing bytes that already hold the correct value.src/platform/linux/gtk_host.c, the first six lines of the function:and then, unconditionally:
Why the information is already correct
The incoming buffer is always the full surface (the function returns 0 unless
rgba8_len == width * height * 4), and the reference rasteriser only re-inks its scissor rect:renderPasscomputes the scissor, clears only that rect on a.load, and culls each command against it. So everything outside the dirty rect is already right in both the source buffer and the cairo buffer. Nothing needs recomputing.Windows already does this
native_sdk_windows_present_gpu_surface_pixelsinsrc/platform/windows/webview2_host.cpphas the exact shape, presumably from #236: asame_bufferguard, finite checks on scale and the four doubles, normalise for negative extents,floor/ceiloutward so a partly covered pixel is converted, clamp to the surface. Porting it to the GTK host is a direct translation, with cairo's premultiply in place of GDI's opaque BGRA and one extra guard: convert in full when the cairo buffer was just reallocated, since it holds nothing.I have been running that port and it behaves. Happy to open a PR if you take them, or to paste the diff here.
One thing that cannot be fixed the same way
The repaint has to stay whole-widget. GTK 4 removed partial invalidation, so
gtk_widget_queue_draw_areais a GTK 3 API and there is no way to ask for less than the widget. Only the conversion narrows. Handing GTK aGdkTextureor a dmabuf instead of a pixel buffer would be the larger fix, but that is a different piece of work and the conversion loop is worth removing on its own.Why it matters beyond the microseconds
The published frame numbers for apps built on this are macOS numbers, because macOS is the only platform that registers a packet presenter, so
ui_app.zig's gate can never be taken on Linux. That is fine and expected. What is not obvious is that the Linux path then also discards the incremental work the runtime did anyway, so the gap is wider than "no GPU" accounts for. I would rather the software path be honestly incremental before anyone measures it.