Skip to content

Linux gpu_surface present discards the dirty rect and converts the whole surface every frame #432

Description

@sepehr-safari

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions