Skip to content

WSL2: render always falls back to SwiftShader (probe: "WebGL unavailable"; /dev/dri/renderD128 not accessible), Windows-native --browser-gpu ~5x faster; segmented capture hangs with >1 worker on Windows #5548

Closed

Description

@ajp-productions

Summary

On WSL2, hyperframes render never uses the GPU, even with an NVIDIA card that WSL can see (/dev/dxg, nvidia-smi and CUDA all work). The auto probe logs browserGpuMode probe → software (WebGL unavailable) and Chrome draws with SwiftShader. --browser-gpu doesn't help either: Chrome's GPU process exits during init. For a long 1080p composition this makes capture CPU-bound: a 7:23 render took 48 min, with the three SwiftShader GPU processes using about 23 of 28 threads.

Running the same CLI natively on Windows on the same machine works: the probe finds the GPU (ANGLE / D3D11) and capture is about 5x faster on the same composition. The workaround is below. On Windows we also hit a hang with segmented capture plus more than one worker (section 4).

Environment

  • Windows 11, build 10.0.26300.9550
  • GPU: NVIDIA GeForce RTX 4070 SUPER, Windows driver 616.92
  • WSL 2.4.13.0, kernel 5.15.167.4-microsoft-standard-WSL2, WSLg 1.0.65 (DISPLAY=:0, WAYLAND_DISPLAY=wayland-0)
  • Ubuntu 22.04.5 LTS, Mesa 23.2.1 (libegl-mesa0, libgl1-mesa-dri with d3d12_dri.so); no Vulkan ICDs installed
  • hyperframes 0.8.153 (npx --yes hyperframes@latest), chrome-headless-shell 152.0.7977.30 (the CLI's own download), Node v22.17.0
  • 28 logical CPUs; 15 GB RAM for WSL, 32 GB on the Windows host
  • Composition: 1920x1080 at 30 fps, 7:23 (13,303 frames), 26 <video> (VP9 alpha WebM and H.264), 165 <audio>, 11 elements with data-color-grading

1. What happens in WSL2

The render log:

GPU: browser GPU (auto-detect)
[hyperframes] browserGpuMode probe → software (WebGL unavailable)
[BrowserManager] Browser launched (HeadlessChrome/152.0.7977.30, beginframe, gl=--use-gl=angle --use-angle=swiftshader, headlessShell=true, platform=linux)
...
rendered in 48m 12.6s
beginframe capture · software gpu · ... · capture 46m 16.5s · encode 45m 54.4s

While it ran, the three --type=gpu-process Chrome processes sat at 600 to 970% CPU each (load average 34 on 28 threads). ffmpeg (libx264, preset slow, CRF 15) used about 25% of one core. The encoder isn't the limit: on its own it encodes 450 of these frames in 4 s.

2. Why: Chrome's EGL path can't open the render node

I launched chrome-headless-shell from the CLI's cache via puppeteer-core, with getHardwareGpuProbeArgs("linux") (--use-gl=angle --use-angle=gl-egl --enable-gpu-rasterization --disable-software-rasterizer) and --enable-logging=stderr --v=1. The GPU process exits:

ERROR:ui/gl/gl_surface_egl.cc:277] No suitable EGL configs found.
ERROR:ui/gl/gl_surface_egl.cc:1021] No suitable EGL configs found for initialization.
ERROR:gpu/config/gpu_info_collector.cc:85] gl::GLContext::CreateOffscreenGLSurface failed
ERROR:gpu/config/gpu_info_collector.cc:593] Could not create surface for info collection.
ERROR:gpu/ipc/service/gpu_init.cc:123] CollectGraphicsInfo failed.
ERROR:components/viz/service/main/viz_main_impl.cc:190] Exiting GPU process due to errors during initialization

Mesa's own EGL fails the same way outside Chrome. I tested with a small ctypes program calling eglInitialize (surfaceless and default platforms) under EGL_LOG_LEVEL=debug:

libEGL warning: failed to open /dev/dri/renderD128: Permission denied
libEGL debug: EGL user error 0x3001 (EGL_NOT_INITIALIZED) in eglInitialize: DRI2: failed to load driver
$ ls -l /dev/dri/ /dev/dxg
crw-rw-rw- 1 root root   10, 127 /dev/dxg
crw-rw---- 1 root video  226,   0 /dev/dri/card0
crw-rw---- 1 root render 226, 128 /dev/dri/renderD128
$ id    # user is in 'video', NOT in 'render'

A default WSL2 Ubuntu user is in video but not render. Mesa therefore can't open the render node it uses to reach the D3D12 Gallium driver, and every EGL route fails.

Every WebGL probe we tried returns WebGL unavailable:

  • --use-angle=gl-egl, --use-angle=gl, --use-angle=vulkan --enable-features=Vulkan (no Vulkan ICD installed) and --use-gl=egl, each with a plain environment and with GALLIUM_DRIVER=d3d12.
  • --use-angle=gl-egl and --use-angle=gles-egl with EGL_PLATFORM set to surfaceless, device, x11 or wayland.
  • --use-angle=gl-egl and --use-angle=gles-egl with EGL_PLATFORM=surfaceless GALLIUM_DRIVER=d3d12 MESA_LOADER_DRIVER_OVERRIDE=d3d12.

Not tested: whether sudo usermod -aG render $USER plus wsl --shutdown lets ANGLE gl-egl reach the RTX through Mesa's d3d12 driver. Fixing the permission should at least get Mesa past the first failure. I'll report back if we try it.

3. Workaround: render from Windows

The same CLI, started from Windows on a copy of the project on C:, with portable Node 22.17.0 and FFmpeg on that process's PATH, and --browser-gpu:

[hyperframes] browserGpuMode probe → hardware (WebGL renderer vendor="Google Inc. (NVIDIA)" renderer="ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 SUPER (0x00002783) Direct3D11 vs_5_0 ps_5_0, D3D11)")
[BrowserManager] Browser launched (HeadlessChrome/152.0.7977.30, screenshot, gl=--use-gl=angle --use-angle=d3d11, headlessShell=true, platform=win32)

Capture rate on the composition's first 15 s (data-duration cut to 15, everything else unchanged), --quality delivery --fps 30:

Setup Capture
WSL2, SwiftShader, 3 workers, HF_SEGMENTED_CAPTURE=true about 3.2 frames/s
Windows, --browser-gpu, 1 worker 8.5 frames/s
Windows, --browser-gpu, 3 workers, HF_SEGMENTED_CAPTURE=false about 16 frames/s

The Windows output matches the WSL render: 450 frames, SSIM 0.989. The only visible difference is text antialiasing, because Windows rasterises fonts with DirectWrite instead of FreeType.

Our wrapper, run from WSL, works like this:

  1. It mirrors the project to C:\Users\<user>\hf-render\<project>. It skips node_modules, renders and similar folders, plus files over 32 MB that no .html / .css references.
  2. It writes a .cmd file that runs npx.cmd --yes hyperframes@latest render ... --browser-gpu with HF_SEGMENTED_CAPTURE=false.
  3. It copies the output back.

We render from a C: copy rather than \\wsl.localhost\... because npx.cmd runs under cmd.exe, which can't start in a UNC directory.

4. Related: Windows, segmented capture with more than one worker hangs page navigation

With HF_SEGMENTED_CAPTURE=true HF_SEGMENT_FRAMES=450 HF_SEGMENT_BROWSER_RECYCLE=1 --workers 3 --browser-gpu on Windows (screenshot capture, since BeginFrame is Linux-only), all three workers load in the same Chrome. The first page initialises; the second page's page.goto never completes:

[BrowserManager] Browser launched (HeadlessChrome/152.0.7977.30, screenshot, gl=--use-gl=angle --use-angle=d3d11, headlessShell=true, platform=win32)
[initSession:screenshot] page.goto start (0ms)
[initSession:screenshot] page.goto complete (649ms)
...
[initSession:screenshot] media + fonts + tailwind ready (46186ms)
[initSession:screenshot] page.goto start (0ms)
  25%  Failed: Navigation timeout of 180000 ms exceeded

What we observed on the same composition and machine:

  • 1 worker with segmented capture: works.
  • 3 workers with HF_SEGMENTED_CAPTURE=false: works. The log shows three Browser launched lines and all three pages load in under 1 s.
  • 3 workers with segmented capture, and the composition stripped to its 2 in-range videos and no <audio>: works.

So the hang needs segmented capture, more than one worker and a media-heavy page together. We haven't isolated the cause. One guess is Chrome's limit of 6 connections per host: the first page's 26 videos and 165 audio elements could hold every connection to the local file server, so the second tab's document request never gets one. #4881 (navigation hangs with data-color-grading on a <video>, ANGLE / D3D11) may be related. This composition has 11 graded elements, but they render fine on Windows without segmented capture.

Suggestions

  1. Make the WSL fallback actionable. When /dev/dxg exists or /proc/version contains microsoft, and the hardware probe fails, the warning could say why (/dev/dri/renderD128 not accessible to the user, or the EGL init error) and suggest the render group or a native Windows render. Today the only output is probe → software (WebGL unavailable), and the --browser-gpu remediation text talks about Docker passthrough.
  2. Document WSL2 in the rendering docs, including that a Windows-native render of the same project uses the GPU.
  3. On Windows, give each worker its own browser when segmented capture is on, or find and fix the second-tab navigation hang above. Segmented capture is what we use in WSL to avoid OOM, so its default on Windows matters.
  4. Minor: the render's local file server listens on all interfaces. On Windows that triggers a Windows Defender Firewall prompt for node.exe the first time. Binding to 127.0.0.1 would avoid it, since the browser only uses localhost.

We can share full logs, the probe scripts and the timing data.

Activity

  1. added
    bugSomething isn't working
    triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.
    difficulty/mediumBounded work requiring subsystem knowledge, related paths, or additional regression fixtures.
    on Oct 13, 2026
  2. jrusso1020 commented on Oct 13, 2026

    @jrusso1020
    Collaborator

    Thanks for the detailed report. On the segmented-capture hang: on Windows all segmented workers share one Chrome, and they all load media from the same local server, so they compete for Chrome's 6 connections per host. On Linux we could reproduce the connection wait but not a full hang. Could you rerun the failing segmented command with PRODUCER_ENABLE_BROWSER_POOL=false set? If that completes, the shared browser is the trigger and we'll give each worker its own.

  3. ajp-productions commented on Oct 13, 2026

    @ajp-productions
    Author

    Thanks @jrusso1020. Results below, all on 0.8.154, same machine as the report (RTX 4070 SUPER, Windows driver 616.92, Chrome 152.0.7977.30).

    1. Requested test: PRODUCER_ENABLE_BROWSER_POOL=false fixes the Windows hang

    The setup is the 15 s copy from the report (all 26 <video> and 165 <audio> kept, 11 with data-color-grading), with HF_SEGMENTED_CAPTURE=true HF_SEGMENT_FRAMES=450 HF_SEGMENT_BROWSER_RECYCLE=1 render --quality delivery --fps 30 --workers 3 --browser-gpu, run on native Windows.

    PRODUCER_ENABLE_BROWSER_POOL Browsers Page loads Result
    unset (default) 1 GPU Chrome page 1 page.goto complete (589ms); page 2 never completes fails: Navigation timeout of 180000 ms exceeded (still reproduces on 0.8.154)
    false 3 GPU Chrome (one per worker) each page.goto complete in about 0.6 s completes: 450 frames, matches the non-segmented render (PSNR 51.5 dB, min 48.1)

    So the shared browser is the trigger.

    One more observation with the pool off: the three worker sessions initialise one after another. Each page.goto start comes only after the previous session's media + fonts + tailwind ready, so any media-readiness wait is paid once per worker in series. In this trimmed copy, 24 out-of-range videos never become ready, so each session waits about 46 s. Capture took 3m18s, against 1m13s for HF_SEGMENTED_CAPTURE=false with 3 workers on 0.8.153, where the three page.goto calls start together.

    2. New bug: segmented capture renders the first frame of a segment ungraded when a data-color-grading video is playing

    Found in a full render. We rendered a 7:23 composition (13,303 frames, HF_SEGMENT_FRAMES=450, WSL, BeginFrame) and compared it frame by frame with a non-segmented Windows render of the same composition. Three single frames, 5400, 6750 and 11250, show the B-roll ungraded (brighter and cooler). The frames on either side are graded:

    frame 6749: mean RGB 78 77 73
    frame 6750: mean RGB 90 96 96   <- segment boundary
    frame 6751: mean RGB 77 76 71
    

    Those are exactly the 3 of the 29 segment boundaries that land inside a graded <video>. No other boundary shows a change, and the non-segmented render is clean at all three.

    Repro. We used the same composition with data-duration set to 30 s. Graded br01 (H.264 1920x1080 30 fps, muted, class="clip") is on screen for frames 682 to 808, and HF_SEGMENT_FRAMES=700 puts a segment boundary inside it. Its grade is {"preset":null,"intensity":1,"adjust":{"contrast":0.3,"highlights":-0.1,"blacks":-0.15,"temperature":0.35,"saturation":-0.35},"details":{"vignette":0.45,"vignetteFeather":0.65,"grain":0.5,"grainSize":0.3,"grainRoughness":0.6},"effects":{"tapeNoise":1,"tapeSpeed":0.5,...}}, written by hyperframes media-treatment --apply.

    Mean RGB at frames 698 to 702:

    Run 698 699 700 701 702
    F1: WSL, BeginFrame, segmented, HF_SEGMENT_BROWSER_RECYCLE=1 72 50 43 71 50 44 91 66 62 71 50 44 71 50 45
    F2: WSL, BeginFrame, segmented, HF_SEGMENT_BROWSER_RECYCLE=0 72 50 43 71 50 44 91 66 62 71 50 44 71 50 45
    F3: Windows, screenshot, segmented, PRODUCER_ENABLE_BROWSER_POOL=false 70 48 42 70 48 42 90 64 61 70 49 43 69 49 44
    F4: Windows, screenshot, HF_SEGMENTED_CAPTURE=false 70 48 42 70 48 42 70 48 42 70 49 43 69 49 44

    All four runs used 3 workers and --quality delivery --fps 30. Both WSL runs logged browserRecycles: 0. Inside br01 the only single-frame flash is frame 700, the only frame that jumps and then returns to its neighbours' colour. The other large frame-to-frame changes (684, 783, 808) are one-way: the clip's entry and its own motion.

    So the flash is independent of platform, capture mode (BeginFrame or screenshot) and browser recycling. What it takes is the first frame of a segment, landing in the middle of a graded video. Our guess is that the first captured frame after a segment's seek is drawn before the grade pipeline has the video frame. We haven't tested whether a worker's first frame in a non-segmented render has the same problem when it lands inside a graded video. #4881 (data-color-grading readiness) might be related.

    Detection tip. check and our cut checks don't catch this. A PSNR comparison against a non-segmented render does: it spikes at exact multiples of HF_SEGMENT_FRAMES.

    3. Full-length timing for the workaround

    Same 7:23 composition (13,303 frames, 1920x1080 at 30 fps, --quality delivery --workers 3):

    Run Total Capture Encode
    WSL, SwiftShader, segmented (450) 48m13s 46m16s streamed alongside capture
    Windows, --browser-gpu, HF_SEGMENTED_CAPTURE=false 18m38s 14m31s 2m16s after capture

    The two outputs pass the same frame-count, loudness (-13.1 LUFS, true peak -1.5 dBFS) and cut checks. The audio correlates at 0.999. The picture differs only in text antialiasing, plus the three ungraded frames above, which only the WSL render has.

    4. On #5560

    Thanks for the WSL diagnostics. You're right about the firewall: the render's file server binds 127.0.0.1. Our suggestion 4 came from misreading the preview port check, sorry for the noise. If it helps, we can test the render group change on this NVIDIA card. Your Intel run couldn't cover that case.

  4. ajp-productions commented on Oct 13, 2026

    @ajp-productions
    Author

    The render group fix on NVIDIA (the case #5560 couldn't test)

    We ran sudo usermod -aG render joe, then wsl --shutdown. After that the user is in render (110) and /dev/dri/renderD128 is readable and writable. Same machine as before: RTX 4070 SUPER, driver 616.92, WSL 2.4.13, Mesa 23.2.1, hyperframes 0.8.154.

    Mesa now reaches the RTX. A ctypes EGL test (system Python) creates a GLES context and reports GL_RENDERER = D3D12 (NVIDIA GeForce RTX 4070 SUPER). Config counts per platform:

    EGL platform total window pbuffer
    surfaceless 64 0 48
    x11 (WSLg) 64 64 64
    wayland (WSLg) 48 48 0

    chrome-headless-shell 152.0.7977.30 (the CLI's browser) still fails, and it fails instantly. This isn't the slow start #5560 saw on Intel. The probe answers "WebGL unavailable" in under 0.2 s, and the GPU process logs the same error as before the fix:

    ERROR:ui/gl/gl_surface_egl.cc:277] No suitable EGL configs found.
    ERROR:gpu/config/gpu_info_collector.cc:85] gl::GLContext::CreateOffscreenGLSurface failed
    ERROR:components/viz/service/main/viz_main_impl.cc:190] Exiting GPU process due to errors during initialization
    

    That holds for --use-angle=gl-egl / gles-egl / gl / vulkan, with EGL_PLATFORM set to x11, wayland, device or surfaceless, and with GALLIUM_DRIVER=d3d12. Our guess: on Mesa's D3D12 driver the surfaceless display has pbuffer configs but no window configs, and the headless shell's config selection doesn't accept that.

    Full Chrome works. Chrome for Testing 139.0.7258.154 (~/.cache/puppeteer) in --headless=new gets WebGL on ANGLE (Microsoft Corporation, D3D12 (NVIDIA GeForce RTX 4070 SUPER), OpenGL ES 3.1) in about 0.1 s. We saw the same with no ozone flag, with --ozone-platform=x11 and with --ozone-platform=wayland, and also with --use-angle=gl, which reports OpenGL 4.2.

    HyperFrames renders on the GPU in WSL with that browser. We rendered the 15 s copy with HYPERFRAMES_BROWSER_PATH=<Chrome 139> --browser-gpu, HF_SEGMENTED_CAPTURE=false, 3 workers, --quality delivery --fps 30:

    [hyperframes] browserGpuMode probe → hardware (WebGL renderer vendor="Google Inc. (Microsoft Corporation)" renderer="ANGLE (Microsoft Corporation, D3D12 (NVIDIA GeForce RTX 4070 SUPER), OpenGL ES 3.1)")
    

    Results against the default headless shell on SwiftShader (same copy, same flags, no HYPERFRAMES_BROWSER_PATH):

    Pages ready Media readiness Capture phase Capture rate once ready
    headless shell, SwiftShader 46.1 s media_readiness_timeout: 3 videos never decode (45 s) 62.6 s about 28 frames/s
    Chrome 139, D3D12 on the RTX 2.5 to 2.8 s all videos ready 23.0 s about 22 frames/s

    Both outputs have 450 frames and match each other (PSNR 49.8 dB, min 47.1, SSIM 0.995). So on WSL with NVIDIA, the GPU path works once the user is in render, but only with full Chrome. On this light opening it doesn't capture faster than SwiftShader. Its gain here came from full Chrome decoding every video, so it never waited out the 45 s readiness timeout the headless shell hit. We haven't run a full-length WSL render on this path.

    Correction to our earlier numbers

    Our 15 s capture rates in the report (WSL 3.2, Windows 1 worker 8.5, Windows 3 workers about 16 frames/s, "about 5x") were wrong as drawing speeds. They divided 450 frames by capture phases that included startup waits: the 45 s media_readiness_timeout on videos the trimmed copy never plays, paid once per worker in series in segmented mode. They also compared segmented runs (HF_SEGMENT_FRAMES=450 on a 450-frame render, which captured at about 8 frames/s, like a single worker) with non-segmented ones. Please disregard them as drawing speeds. Like for like (3 workers, no segments, measured after the pages are ready), this light opening captures at about 28 frames/s on WSL SwiftShader and about 22 on WSL D3D12 (full Chrome). Windows D3D11 with screenshot capture comes out around 16, estimated from the capture phase minus the 46 s wait.

    The reliable comparison is the full-length one in our previous comment. The 7:23 composition captured in 46m16s on WSL SwiftShader (segmented) against 14m31s on Windows --browser-gpu (no segments), about 3.2x, with a total of 48m13s against 18m38s. The gain comes from the heavy sections (graded B-roll, blurs, split screens), where SwiftShader dropped to about 2 frames/s.

  5. ajp-productions commented on Oct 14, 2026

    @ajp-productions
    Author

    Retested on 0.8.155, same machine (Windows 11, RTX 4070 SUPER, driver 616.92; WSL 2.4.13, user now in render). Thanks for the quick turnaround.

    #5565, the Windows segmented hang: fixed. The exact command from the report, with default settings and no PRODUCER_ENABLE_BROWSER_POOL (HF_SEGMENTED_CAPTURE=true HF_SEGMENT_FRAMES=450 HF_SEGMENT_BROWSER_RECYCLE=1 render --quality delivery --fps 30 --workers 3 --browser-gpu), now launches one GPU Chrome per worker. Each page.goto completes in about 0.6 s, and the render finishes with all 450 frames. On 0.8.154 it timed out after 180 s. One small thing remains: the worker sessions still initialise one after another (each page.goto start follows the previous session's media + fonts + tailwind ready). That only costs time when readiness is slow.

    #5560, the WSL diagnostics: working. In auto mode, with the user in render:

    [hyperframes] browserGpuMode probe → software (WebGL unavailable)
    [hyperframes] WSL: the GPU check found no hardware GPU, so this render uses software WebGL (SwiftShader) and runs at CPU speed.
      /dev/dri/renderD128 is readable and writable, so file permissions are not the cause.
      Pass --no-browser-gpu to skip the GPU check and render in software.
    

    A suggestion for that branch: once permissions are ruled out, it currently gives no route to the GPU. On NVIDIA (our comment above) the two routes that work are rendering from native Windows, and HYPERFRAMES_BROWSER_PATH pointing at full Chrome in --headless=new, which reaches the GPU through Mesa D3D12 where chrome-headless-shell fails with "No suitable EGL configs found". The native Windows line that the usermod branch prints would help here too.

    Still open: the ungraded first frame of a segment. It reproduces unchanged on 0.8.155. We used the 30 s repro from our earlier comment (graded br01 on screen for frames 682 to 808) with HF_SEGMENTED_CAPTURE=true HF_SEGMENT_FRAMES=700, 3 workers, --browser-gpu, on native Windows:

    frame 698: mean RGB 71 49 42
    frame 699: mean RGB 70 49 43
    frame 700: mean RGB 90 65 61   <- segment boundary
    frame 701: mean RGB 70 49 43
    frame 702: mean RGB 70 50 44
    

    The two merged PRs don't touch it, so this is just confirmation it's still there. Happy to run a build against it. Until it's fixed we render without segments when a composition has data-color-grading video.

  6. miguel-heygen commented on Oct 14, 2026

    @miguel-heygen
    Collaborator

    Thanks for retesting, and for running the render group test on NVIDIA; that pinned it down.

    Two more fixes are up:

    The ungraded first frame of a segment is being worked on next.

    We can't check #5574 on NVIDIA ourselves. Would you try the branch on your machine?

    git clone --depth 1 -b fix/wsl-gpu-full-chrome https://github.com/heygen-com/hyperframes.git hf-wsl-gpu
    cd hf-wsl-gpu && bun install && bun run build
    npx @puppeteer/browsers install chrome@stable --path ~/.cache/puppeteer
    cd /path/to/project && node /path/to/hf-wsl-gpu/packages/cli/bin/hyperframes.mjs render --output out.mp4

    You should see browserGpuMode probe → software (WebGL unavailable) for the headless shell, then browserGpuMode probe → hardware (full Chrome: ...) and a Browser launched (..., screenshot, ...) line.

  7. ajp-productions commented on Oct 14, 2026

    @ajp-productions
    Author

    Tested #5574 (fix/wsl-gpu-full-chrome at 600ffd0) on the NVIDIA machine: Windows 11, RTX 4070 SUPER, driver 616.92, WSL 2.4.13, Ubuntu 22.04, Mesa 23.2.1, user in render. Short version: the fallback works, but only with Chrome 149 or older. Every Chrome from 150 on fails the same way the headless shell does, and that includes chrome@stable (155 today).

    1. As instructed: no GPU

    bun install put Chrome for Testing 152.0.7977.42 in ~/.cache/puppeteer (Puppeteer's postinstall). resolveFullChromePath takes the newest version there, so the render used it:

    [hyperframes] browserGpuMode probe → software (WebGL unavailable)
    [hyperframes] browserGpuMode probe → software (full Chrome: WebGL unavailable)
    [hyperframes] WSL: the GPU check found no hardware GPU, so this render uses software WebGL (SwiftShader) and runs at CPU speed.
    

    It rendered fine on SwiftShader: 450 frames, 2m15s.

    2. With Chrome 139 as the newest full Chrome: works exactly as described

    We moved 152 out of the cache, leaving 139.0.7258.154, and ran the same command:

    [hyperframes] browserGpuMode probe → software (WebGL unavailable)
    [hyperframes] browserGpuMode probe → hardware (full Chrome: WebGL renderer vendor="Google Inc. (Microsoft Corporation)" renderer="ANGLE (Microsoft Corporation, D3D12 (NVIDIA GeForce RTX 4070 SUPER), OpenGL ES 3.1)")
    [BrowserManager] Browser launched (Chrome/139.0.7258.154, screenshot, gl=--use-gl=angle --use-angle=gl-egl, headlessShell=true, platform=linux)
    

    It rendered 450 frames in 46.2s, against 2m15s for run 1, and matches the SwiftShader output (PSNR 49.5 dB, min 46.9). GPU load peaked at 23%.

    Caveat on that speed-up: this is our trimmed 15 s copy. Most of the gap is the headless shell's 45 s media_readiness_timeout per session on videos outside the trimmed duration, which full Chrome never hits. Measured after the pages are ready, all runs capture this light opening at about 23 to 31 frames/s. We haven't run a heavy composition in WSL on the GPU.

    3. Bisect: Chrome 150 is where it breaks

    We used browsers install chrome@N / chrome-headless-shell@N into a scratch path and probed WebGL with the CLI's Linux hardware flags (--use-gl=angle --use-angle=gl-egl --enable-gpu-rasterization --ignore-gpu-blocklist --disable-software-rasterizer):

    Build WebGL renderer
    full Chrome 139.0.7258.154 D3D12 (NVIDIA GeForce RTX 4070 SUPER)
    full Chrome 146.0.7680.165 D3D12 (NVIDIA GeForce RTX 4070 SUPER)
    full Chrome 149.0.7827.155 D3D12 (NVIDIA GeForce RTX 4070 SUPER)
    chrome-headless-shell 149.0.7827.155 D3D12 (NVIDIA GeForce RTX 4070 SUPER)
    full Chrome 150.0.7871.124 WebGL unavailable
    full Chrome 151.0.7922.138 WebGL unavailable
    full Chrome 152.0.7977.42 WebGL unavailable
    full Chrome 155.0.8059.79 (chrome@stable today) WebGL unavailable
    chrome-headless-shell 152.0.7977.30 (the CLI's) WebGL unavailable

    From 150 on, the GPU process fails at initialisation (CreateOffscreenGLSurface failed → Exiting GPU process due to errors during initialization). On 152 the log also shows the original report's gl_surface_egl.cc: No suitable EGL configs found. --headless=new, --ozone-platform=x11|wayland, --use-angle=gl|vulkan and EGL_PLATFORM overrides made no difference. Mesa's surfaceless display on D3D12 offers 48 pbuffer configs and 0 window configs (x11: 64 / 64), so something in 150 seems to have changed which configs it accepts.

    So the variable is the Chrome major, not headless shell vs full Chrome. With 0.8.155 released and HYPERFRAMES_BROWSER_PATH pointed at chrome-headless-shell 149:

    [hyperframes] browserGpuMode probe → hardware (WebGL renderer vendor="Google Inc. (Microsoft Corporation)" renderer="ANGLE (Microsoft Corporation, D3D12 (NVIDIA GeForce RTX 4070 SUPER), OpenGL ES 3.1)")
    [BrowserManager] Browser launched (HeadlessChrome/149.0.7827.155, beginframe, gl=--use-gl=angle --use-angle=gl-egl, headlessShell=true, platform=linux)
    beginframe capture · hardware gpu
    

    That's BeginFrame capture on the GPU in WSL. The output matches (450 frames, PSNR 49.6 dB) and GPU load peaked at 73%.

    Suggestions

    1. Try every cached full Chrome, newest first, until one answers with hardware, not only the newest. Each probe took under a second here. As it stands, the fallback picks a failing Chrome on any machine that has a recent one, and the note's chrome@stable installs one.
    2. Or cap Chrome at 149 under WSL with Mesa D3D12. The existing chromeMajorCeiling mechanism could apply to the headless shell too, which keeps BeginFrame. In the note, chrome@149 would be the install line that actually works today.
    3. This looks like a Chromium regression in 150 for EGL on Mesa's D3D12 (WSLg) driver. We haven't filed it upstream. Happy to run a probe build if you have one.

    Two minor notes:

    • bun run build on Node 22.17.0 failed in @hyperframes/sdk-playground (ERR_UNKNOWN_FILE_EXTENSION ".ts" for packages/sdk/src/adapters/fs.ts). It looks like it needs Node's type stripping (22.18 and later). cli, engine and producer built fine.
    • Also on 0.8.155, the ungraded first frame of a segment is still there (previous comment). Thanks for queuing it.
  8. miguel-heygen commented on Oct 14, 2026

    @miguel-heygen
    Collaborator

    Thanks for the bisect. It found the cause: since Chrome 150, Chrome picks its Wayland graphics backend whenever WAYLAND_DISPLAY is set, and WSLg always sets it. That backend has no offscreen configs on Mesa's D3D12 driver, so the GPU process exits. Without the variable, Chrome uses X11 graphics, which works. It is tracked upstream at https://issues.chromium.org/issues/567222741.

    0.8.157 is on npm with the fix (#5591). Under WSL, when the GPU check finds no GPU and WAYLAND_DISPLAY is set, HyperFrames checks once more without it. If that reaches the GPU, renders launch that way. Otherwise nothing changes. It also removes the full-Chrome fallback from #5574, which could not help.

    Could you try it on the NVIDIA machine, with the default GPU setting (no --browser-gpu, which skips the check)? Clear any HYPERFRAMES_BROWSER_PATH first:

    npx [email protected] render --output out.mp4

    Expected in the log:

    [hyperframes] browserGpuMode probe → software (WebGL unavailable)
    [hyperframes] browserGpuMode probe → hardware (without WAYLAND_DISPLAY: WebGL renderer ... D3D12 (NVIDIA ...) ...)
    [BrowserManager] Browser launched (HeadlessChrome/152..., beginframe, ...)
    

    Our WSL test PC has an Intel GPU whose X11 graphics are broken, so it never takes the GPU path; your machine is the one that can confirm it. A render time against your 149 runs would help too.

  9. ajp-productions commented on Oct 14, 2026

    @ajp-productions
    Author

    Confirmed on 0.8.157, NVIDIA machine (RTX 4070 SUPER, driver 616.92, WSL 2.4.13 with WSLg, user in render, no HYPERFRAMES_BROWSER_PATH). Everything we reported in this issue is fixed on our side.

    #5591: WSL reaches the GPU with the CLI's own Chrome 152

    npx [email protected] render --output out.mp4, default GPU setting:

    [hyperframes] browserGpuMode probe → software (WebGL unavailable)
    [hyperframes] browserGpuMode probe → hardware (without WAYLAND_DISPLAY: WebGL renderer vendor="Google Inc. (Microsoft Corporation)" renderer="ANGLE (Microsoft Corporation, D3D12 (NVIDIA GeForce RTX 4070 SUPER), OpenGL ES 3.1)")
    [BrowserManager] Browser launched (HeadlessChrome/152.0.7977.30, beginframe, gl=--use-gl=angle --use-angle=gl-egl, headlessShell=true, platform=linux)
    ...
    beginframe capture · hardware gpu
    

    That's exactly the expected sequence: the headless shell's own Chrome 152, BeginFrame on the RTX, no older Chrome needed. The 450-frame output matches the SwiftShader render of the same copy (PSNR 49.6 dB, min 47.1, SSIM 0.995).

    Render times. On our trimmed 15 s copy the three setups are a tie, because that copy's 45 s media-readiness timeouts dominate and the content is light:

    15 s copy, defaults Total Capture rate after pages ready
    0.8.157, GPU (without WAYLAND_DISPLAY) 2m15.7s about 29 frames/s
    0.8.155 + HYPERFRAMES_BROWSER_PATH = headless shell 149 (our earlier run) 2m12.2s about 29 frames/s
    0.8.157, --no-browser-gpu 2m18.3s about 33 frames/s

    On heavier content the GPU path pays off. All rows below are WSL, --quality delivery --fps 30 --workers 3:

    Composition WSL GPU, 0.8.157 (BeginFrame) WSL software
    30 s copy with graded B-roll, segmented (HF_SEGMENT_FRAMES=700) 2m29s 7m25s (0.8.157 --no-browser-gpu, screenshot capture)
    Full 7:23, 13,308 frames, segmented 450 11m49s (capture 9m52s) 48m13s (capture 46m16s; 0.8.153, BeginFrame on SwiftShader, 13,303 frames)

    The full-length WSL GPU render is now faster than our native Windows route on the same composition: 17m18s, with D3D11 and screenshot capture, no segments. It passes our frame-count, loudness and cut checks identically, and compared frame by frame with the Windows render it gives SSIM 0.991, with only text antialiasing differing (FreeType vs DirectWrite).

    #5575: ungraded first frame of a segment, fixed

    The 30 s repro (graded br01 on frames 682 to 808, HF_SEGMENT_FRAMES=700), mean RGB at frames 698 / 699 / 700 / 701 / 702:

    Run on 0.8.157 698 699 700 701 702
    WSL, GPU, BeginFrame 72 50 43 71 50 44 71 50 44 71 50 44 71 50 45
    WSL, --no-browser-gpu, screenshot 72 50 43 71 50 44 72 50 44 71 50 44 71 50 45
    Windows, --browser-gpu, screenshot 71 50 43 71 50 43 71 50 43 71 50 44 70 51 45

    On 0.8.155 frame 700 was 90 65 61. The full-length render above also has no ungraded frame at any of its 29 segment boundaries; 0.8.153 had three (5400 / 6750 / 11250).

    #5573 and #5565: segmented workers on Windows

    On Windows with 0.8.157 and default settings (no PRODUCER_ENABLE_BROWSER_POOL), the three workers now start together, one Chrome each:

    [initSession:screenshot] page.goto start (0ms)   x3
    [initSession:screenshot] page.goto complete (650ms) / (700ms) / (863ms)
    [initSession:screenshot] media + fonts + tailwind ready (46799ms) / (46739ms) / (46800ms)
    

    The readiness waits overlap. The same 30 s segmented test took 3m33.9s against 5m03.1s on 0.8.155.

    Thanks for the fast turnaround on all four fixes. From our side this issue can be closed.

  10. jrusso1020 commented on Oct 24, 2026

    @jrusso1020
    Collaborator

    Closing as fixed. The WSL2 GPU probe and Windows capture fixes landed in #5560, #5565, #5573, #5574, #5575 and #5591, and you confirmed on 0.8.157 that everything reported here is fixed on your side. Thanks for the detailed report.

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

    area/observabilitybugSomething isn't workingdifficulty/mediumBounded work requiring subsystem knowledge, related paths, or additional regression fixtures.triage/readyAccepted scope, current evidence, acceptance criteria, and validation path are recorded.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions