Skip to content

[BUG] Windows WebKit: WebGL canvas blank in page.screenshot() after setViewportSize() resizes the drawing buffer (macOS WebKit unaffected) #42885

Description

@ibrews

Context

While debugging a WebXR test suite, we isolated a repeatable bug in Playwright's Windows WebKit: after page.setViewportSize() resizes a canvas's WebGL drawing buffer, page.screenshot() can render the page background where the canvas should be — even though the WebGL context is healthy (no gl.getError(), no gl.isContextLost()) and the canvas keeps rendering. Only a page reload, or a WebGL context loss/restore, brings the canvas back in future screenshots.

We reduced this to a ~20-line, dependency-free repro and ran the identical script on Windows and macOS with the same Playwright/WebKit build. Windows fails 5/5 runs; macOS passes 5/5 runs. This strongly suggests a Windows-port-specific issue in the WebKit screenshot path, not an app bug.

Environment

  • Playwright: 1.63.0
  • WebKit build: 26.6 (playwright webkit v2359) — same build on both platforms
  • Failing: Windows, Microsoft Windows [Version 10.0.26200.9550], win32
  • Passing: macOS, Darwin, arm64 (Apple Silicon)

Minimal repro

index.html (served locally, no external dependencies — raw WebGL, no framework):

<!doctype html>
<html><body style="margin:0">
<canvas id="c"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl')
function resize() {
  const c = gl.canvas
  c.width = innerWidth
  c.height = innerHeight
  gl.viewport(0, 0, c.width, c.height)
}
addEventListener('resize', resize)
resize()
function frame() {
  gl.clearColor(1, 0, 0, 1)
  gl.clear(gl.COLOR_BUFFER_BIT)
  requestAnimationFrame(frame)
}
frame()
window.__ready = true
</script>
</body></html>

repro.mjs:

import { webkit } from 'playwright'
import { createServer } from 'node:http'
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'

const html = await readFile(fileURLToPath(new URL('./index.html', import.meta.url)))
const server = createServer((req, res) => {
  res.setHeader('Content-Type', 'text/html')
  res.end(html)
}).listen(4321, '127.0.0.1')

const browser = await webkit.launch()
const page = await browser.newPage({ viewport: { width: 390, height: 664 } })
await page.goto('http://127.0.0.1:4321')
await page.waitForFunction(() => window.__ready === true)
await page.waitForTimeout(300)

await page.setViewportSize({ width: 390, height: 844 })
await page.waitForTimeout(300)

const png = await page.screenshot()
const centerPixel = await page.evaluate((base64) => new Promise((resolve) => {
  const img = new Image()
  img.onload = () => {
    const c = document.createElement('canvas')
    c.width = img.width; c.height = img.height
    const ctx = c.getContext('2d')
    ctx.drawImage(img, 0, 0)
    resolve([...ctx.getImageData(c.width >> 1, c.height >> 1, 1, 1).data])
  }
  img.src = 'data:image/png;base64,' + base64
}), png.toString('base64'))

console.log('browser:', browser.version(), 'platform:', process.platform)
console.log('composed screenshot center pixel:', centerPixel)
console.log('expected [255,0,0,255] (red); anything else (e.g. white/page bg) means the canvas went blank in the screenshot')

await browser.close()
server.close()

Steps to reproduce

  1. npm i playwright@1.63.0 && npx playwright install webkit
  2. Save the two files above in the same directory.
  3. node repro.mjs

Expected

composed screenshot center pixel: [ 255, 0, 0, 255 ] on every run, on every platform — the canvas is continuously cleared to red, so the screenshot taken 300ms after the resize should show red.

Actual

  • macOS (Darwin, arm64): [ 255, 0, 0, 255 ] on 5/5 runs. Correct.
  • Windows (10.0.26200.9550, win32): [ 255, 255, 255, 255 ] on 5/5 runs. The screenshot shows white (the page background) instead of the red canvas.

The WebGL context itself is unaffected — reading back the live GPU buffer with gl.readPixels() immediately before the screenshot still returns the correct color on Windows; only the screenshot's composited image is wrong. A page reload restores correct screenshots until the next resize.

What we ruled out (on the larger app this was first found in, before reducing to this minimal repro)

  • A drawing-buffer size change is the trigger — 40/40 buffer resizes (taller, shorter, rotated, narrower, even back to the original size) went blank; when only the camera aspect changed and the buffer size stayed the same, screenshots stayed correct.
  • It isn't transient — the canvas stayed blank in screenshots for 100+ frames, after swapping the render loop for a bare gl.clear() loop, and after a second resize.
  • App/GL state isn't the cause — resetting pixel-store settings, all texture/renderbuffer/sampler/VAO bindings, scissor, masks, or antialias/alpha/premultipliedAlpha/preserveDrawingBuffer/powerPreference all still failed.
  • Single-run testing is misleading on a complex page — a bisection needs repeats (REPS=3+) before treating a variant as causal.

Hypothesis (unconfirmed — we did not instrument WebKit internals)

Playwright's WebKit screenshots go through Page.snapshotRect, a software paint path with compositing layers flattened, rather than reading the GPU compositor's actual output. In that path, HTMLCanvasElement::paint appears to skip drawing when it believes the WebGL display buffer is transparent/stale after a resize (possibly related to surfaceBufferToImageBuffer caching a transparent buffer when a display-buffer read fails). Whether the real on-screen (non-headless, non-screenshot) composition is also affected is unverified — this repro only exercises page.screenshot().

Workarounds we're aware of (neither is acceptable for our use case)

  • Never resize the drawing buffer after creation (breaks correct aspect-ratio/rotation handling).
  • Force a WebGL context loss + restore on every resize (expensive, and not something we want in a real device path — only useful for satisfying this screenshot check).

Happy to provide the full probe harness (67 variants tested) if useful — trimmed here to the smallest repro that reliably shows the platform difference.

No activity

Activity on this issue will appear here.

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions