microsoft / vscode

Visual Studio Code
https://code.visualstudio.com
MIT License
161.73k stars 28.43k forks source link

WebGPU-based renderer for the editor #221145

Open Tyriar opened 1 month ago

Tyriar commented 1 month ago

We're finally starting to look at implementing a WebGPU-based rendering in monaco, similar to what xterm.js uses. This issue is used to track all the work which is expected to take several months.

### Tasks
- [ ] https://github.com/microsoft/vscode/issues/221202
- [ ] https://github.com/microsoft/vscode/issues/221210
- [ ] https://github.com/microsoft/vscode/issues/221211
- [ ] https://github.com/microsoft/vscode/issues/221216
- [ ] https://github.com/microsoft/vscode/issues/225428
- [ ] https://github.com/microsoft/vscode/issues/225722

Related issues

Here are some historical links that might be useful:


Below copied from https://github.com/microsoft/vscode-internalbacklog/issues/4906

GPU-based rendering

branch: tyriar/gpu_exploration

How GPU rendering works

It works by assembling array buffers which represent commands to run on the GPU, these are filled on the CPU with information like the texture to use (chracter, fg, bg), location, offset, etc. xterm.js for example allocates a cols x rows array buffer that represents the viewport only and updates it on every frame where the viewport changes.

There are 2 types of shaders:

How the prototype works

The WebGPU prototype works by pre-allocating a buffer that represents up to 3000 lines in a file with a maximum column length of 200. The buffers* are lazily filled in based on what's the viewport. Meaning once a line is loaded, it doesn't need to be modified again. I think it updates more aggressively currently than needed due to my lack of knowledge around finding dirty lines in Monaco.

@vertex fn vs(
    vert: Vertex,
    @builtin(instance_index) instanceIndex: u32,
    @builtin(vertex_index) vertexIndex : u32
) -> VSOutput {
    let dynamicUnitInfo = dynamicUnitInfoStructs[instanceIndex];
    let spriteInfo = spriteInfo[u32(dynamicUnitInfo.textureIndex)];

    var vsOut: VSOutput;
    // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1
    vsOut.position = vec4f(
        (((vert.position * vec2f(2, -2)) / uniforms.canvasDimensions)) * spriteInfo.size + dynamicUnitInfo.position + ((spriteInfo.origin * vec2f(2, -2)) / uniforms.canvasDimensions) + ((scrollOffset.offset * 2) / uniforms.canvasDimensions),
        0.0,
        1.0
    );

    // Textures are flipped from natural direction on the y-axis, so flip it back
    vsOut.texcoord = vert.position;
    vsOut.texcoord = (
        // Sprite offset (0-1)
        (spriteInfo.position / textureInfoUniform.spriteSheetSize) +
        // Sprite coordinate (0-1)
        (vsOut.texcoord * (spriteInfo.size / textureInfoUniform.spriteSheetSize))
    );

    return vsOut;
}
@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
    return textureSample(ourTexture, ourSampler, vsOut.texcoord);
}

Texture atlas

Glyphs are rendered on the CPU using the browser's canvas 2d context to draw the characters into a texture atlas. The texture atlas can have multiple pages, this is an optimization problem as uploading images is relative expensive. xterm.js creates multiple small texture atlas pages, allocates using a shelf allocator and eventually merged them into larger immutable pages as they're more expensive to upload.

Currently the prototype uses a single large texture atlas page, but it warms it up in idle callbacks for the current font and all theme token colors in the background (using the TaskQueue xterm.js util).

image

Memory usage

text_data_buffer: [wgslX, wgslY, textureIndex, ...]

texture_atlas_buffer: [positionX, positionY, sizeX, sizeY, offsetX, offsetY, ...]

textureIndex in text_data_buffer maps to texture_atlas_buffer[textureIndex * 6]

In the above, each text_data_buffer cell is 12 bytes (3x 32-bit floats), so 3000x200 would be:

3000 * 200 * 12 = 7.2MB

This is pretty insignificant for a modern GPU.

* Double buffering is used as the GPU locks array buffers until it's done with it.

Scrolling

The prototype currently scrolls extremely smoothly as at most a viewport worth of data is filled but often no viewport data will change. Then we just need to update the scroll offset so the shadow knows which cells to render.

Input

So far, the above is highly optimized for readonly scrolling. For input/file changes there are a few cases we need to target. We essentially want to get these updates to take as little CPU time as possible, even if that means leaving stale and no-longer referenced data in the fixed buffers.

Adding new lines or deleting lines

This could be supported by uploading a map whose job is to map line numbers with the index in the fixed buffer:

image

That way we only need to update indexes, not the whole line data.

Inserting characters

Simple O(n) solution is to just update the entire line. We could do tricks to make this faster but it might not be worth the effort if line length is fixed.

Fixed buffers and long lines

My plan for how the characters will be send to the GPU is to have 1 or more fixed width buffers (eg. 80, 200?) with maps that point to indexes dynamically as described in the input section and then another more dynamic buffer which supports lines of arbitrary length. This dynamic buffer will be a little less optimized as it's the edge case when coding. The fixed buffers could also be dynamically allocated based on the file to save some memory.

Other things we could do

Test results

These were done on terminalInstance.ts. Particularly slow frames of the test are showed.

The tyriar/gpu_exploration tests disabled all dom rendering (lines, sticky scroll, etc.) to get an idea of how fast things could be without needed to perform layouts on each frame. It's safe to assume that rendering other components would be less than or equal to the time of the most complex component (minimap is similar, but could potentially share data as well).

Scroll to top command

M2 Pro Macbook main

image

M2 Pro Macbook tyriar/gpu_exploration (all dom rendering disabled)

image

Windows gaming PC main

image

Windows gaming PC tyriar/gpu_exploration (all dom rendering disabled)

image

Scrolling with small text on a huge viewport

fontSize 6, zoomLevel -4

M2 Pro Macbook main

image

M2 Pro Macbook tyriar/gpu_exploration (all dom rendering disabled)

image

Windows gaming PC main

image

Windows gaming PC tyriar/gpu_exploration (all dom rendering disabled)

image

Very long line

Long lines aren't supported in the gpu renderer currently

Shaders run in parallel to microtasks and layout

The sample below from the Windows scroll to top test above demonstrates how the shaders execute in parallel with layout, as opposed to all after layout.

Before:

image

After:

image


Harfbuzz shaping engine is used by lots of programs including Chromium to determine various things about text rendering. This might be needed for good RTL/ligature/grapheme rendering.

IllusionMH commented 1 month ago

Would it be possible to re-upload images to this repo, as they aren't available for regular users now, or they are on a need-to-know basis?

UPD. Thanks! Hiding comment, as it's a bit off topic.

Tyriar commented 1 month ago

@IllusionMH opps, doing it now 👍

Tyriar commented 3 days ago

Update on my end for last week. WIP branch https://github.com/microsoft/vscode/pull/225413

General

Rasterization

Texture atlas

Explorations