Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement rectangle renderer and gpu rulers view part #228632

Merged
merged 15 commits into from
Sep 17, 2024
10 changes: 5 additions & 5 deletions src/vs/editor/browser/gpu/fullFileRenderStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra

let tokens: IViewLineTokens;

const activeWindow = getActiveWindow();
const dpr = getActiveWindow().devicePixelRatio;

// Update scroll offset
const scrollTop = this._context.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio;
const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex];
scrollOffsetBuffer[1] = scrollTop;
scrollOffsetBuffer[0] = this._context.viewLayout.getCurrentScrollLeft() * dpr;
scrollOffsetBuffer[1] = this._context.viewLayout.getCurrentScrollTop() * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer);

// Update cell data
Expand Down Expand Up @@ -235,14 +235,14 @@ export class FullFileRenderStrategy extends Disposable implements IGpuRenderStra
glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata);

// TODO: Support non-standard character widths
screenAbsoluteX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * activeWindow.devicePixelRatio);
screenAbsoluteX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * dpr);
screenAbsoluteY = (
Math.ceil((
// Top of line including line height
viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] +
// Delta to top of line after line height
Math.floor((viewportData.lineHeight - this._context.configuration.options.get(EditorOption.fontSize)) / 2)
) * activeWindow.devicePixelRatio)
) * dpr)
);
zeroToOneX = screenAbsoluteX / this._canvas.width;
zeroToOneY = screenAbsoluteY / this._canvas.height;
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct VSOutput {
};

// Uniforms
@group(0) @binding(${BindingId.ViewportUniform}) var<uniform> layoutInfo: LayoutInfo;
@group(0) @binding(${BindingId.LayoutInfoUniform}) var<uniform> layoutInfo: LayoutInfo;
@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var<uniform> atlasDims: vec2f;
@group(0) @binding(${BindingId.ScrollOffset}) var<uniform> scrollOffset: ScrollOffset;

Expand All @@ -67,7 +67,7 @@ struct VSOutput {
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)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((scrollOffset.offset + layoutInfo.viewportOffset) * 2) / layoutInfo.canvasDims),
(((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((layoutInfo.viewportOffset - scrollOffset.offset * vec2(1, -1)) * 2) / layoutInfo.canvasDims),
0.0,
1.0
);
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/browser/gpu/gpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const enum BindingId {
Cells,
TextureSampler,
Texture,
ViewportUniform,
LayoutInfoUniform,
AtlasDimensionsUniform,
ScrollOffset,
}
Expand Down
17 changes: 16 additions & 1 deletion src/vs/editor/browser/gpu/objectCollectionBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface IObjectCollectionBuffer<T extends ObjectCollectionBufferPropert
* The size of the used portion of the view (in float32s).
*/
readonly viewUsedSize: number;
/**
* The number of entries in the buffer.
*/
readonly entryCount: number;

/**
* Fires when the buffer is modified.
Expand All @@ -53,6 +57,7 @@ export interface IObjectCollectionBuffer<T extends ObjectCollectionBufferPropert
export interface IObjectCollectionBufferEntry<T extends ObjectCollectionBufferPropertySpec[]> extends IDisposable {
set(propertyName: T[number]['name'], value: number): void;
get(propertyName: T[number]['name']): number;
setRaw(data: ArrayLike<number>): void;
}

export function createObjectCollectionBuffer<T extends ObjectCollectionBufferPropertySpec[]>(
Expand All @@ -72,6 +77,9 @@ class ObjectCollectionBuffer<T extends ObjectCollectionBufferPropertySpec[]> ext
get viewUsedSize() {
return this._entries.size * this._entrySize;
}
get entryCount() {
return this._entries.size;
}

private readonly _propertySpecsMap: Map<string, ObjectCollectionBufferPropertySpec & { offset: number }> = new Map();
private readonly _entrySize: number;
Expand All @@ -86,7 +94,7 @@ class ObjectCollectionBuffer<T extends ObjectCollectionBufferPropertySpec[]> ext
) {
super();

this.view = new Float32Array(capacity * 2);
this.view = new Float32Array(capacity * propertySpecs.length);
this.buffer = this.view.buffer;
this._entrySize = propertySpecs.length;
for (let i = 0; i < propertySpecs.length; i++) {
Expand Down Expand Up @@ -159,4 +167,11 @@ class ObjectCollectionBufferEntry<T extends ObjectCollectionBufferPropertySpec[]
get(propertyName: T[number]['name']): number {
return this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset];
}

setRaw(data: ArrayLike<number>): void {
if (data.length !== this._propertySpecsMap.size) {
throw new Error(`Data length ${data.length} does not match the number of properties in the collection (${this._propertySpecsMap.size})`);
}
this._view.set(data, this.i * this._propertySpecsMap.size);
}
}
274 changes: 274 additions & 0 deletions src/vs/editor/browser/gpu/rectangleRenderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { getActiveWindow } from '../../../base/browser/dom.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { ViewEventHandler } from '../../common/viewEventHandler.js';
import type { ViewScrollChangedEvent } from '../../common/viewEvents.js';
import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';
import type { ViewContext } from '../../common/viewModel/viewContext.js';
import { GPULifecycle } from './gpuDisposable.js';
import { observeDevicePixelDimensions, quadVertices } from './gpuUtils.js';
import { createObjectCollectionBuffer, type IObjectCollectionBuffer, type IObjectCollectionBufferEntry } from './objectCollectionBuffer.js';
import { RectangleRendererBindingId, rectangleRendererWgsl } from './rectangleRenderer.wgsl.js';

export type RectangleRendererEntrySpec = [
{ name: 'x' },
{ name: 'y' },
{ name: 'width' },
{ name: 'height' },
{ name: 'red' },
{ name: 'green' },
{ name: 'blue' },
{ name: 'alpha' },
];

export class RectangleRenderer extends ViewEventHandler {

private _device!: GPUDevice;
private _renderPassDescriptor!: GPURenderPassDescriptor;
private _renderPassColorAttachment!: GPURenderPassColorAttachment;
private _bindGroup!: GPUBindGroup;
private _pipeline!: GPURenderPipeline;

private _vertexBuffer!: GPUBuffer;
private _shapeBindBuffer!: GPUBuffer;

private _scrollOffsetBindBuffer!: GPUBuffer;
private _scrollOffsetValueBuffer!: Float32Array;

private _initialized: boolean = false;
private _scrollChanged: boolean = true;

private readonly _shapeCollection: IObjectCollectionBuffer<RectangleRendererEntrySpec> = this._register(createObjectCollectionBuffer([
{ name: 'x' },
{ name: 'y' },
{ name: 'width' },
{ name: 'height' },
{ name: 'red' },
{ name: 'green' },
{ name: 'blue' },
{ name: 'alpha' },
], 32));

constructor(
private readonly _context: ViewContext,
private readonly _canvas: HTMLCanvasElement,
private readonly _ctx: GPUCanvasContext,
device: Promise<GPUDevice>,
) {
super();

this._context.addEventHandler(this);

this._initWebgpu(device);
}

private async _initWebgpu(device: Promise<GPUDevice>) {

// #region General

this._device = await device;

if (this._store.isDisposed) {
return;
}

const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
this._ctx.configure({
device: this._device,
format: presentationFormat,
alphaMode: 'premultiplied',
});

this._renderPassColorAttachment = {
view: null!, // Will be filled at render time
loadOp: 'load',
storeOp: 'store',
};
this._renderPassDescriptor = {
label: 'Monaco rectangle renderer render pass',
colorAttachments: [this._renderPassColorAttachment],
};

// #endregion General

// #region Uniforms

let layoutInfoUniformBuffer: GPUBuffer;
{
const enum Info {
FloatsPerEntry = 6,
BytesPerEntry = Info.FloatsPerEntry * 4,
Offset_CanvasWidth____ = 0,
Offset_CanvasHeight___ = 1,
Offset_ViewportOffsetX = 2,
Offset_ViewportOffsetY = 3,
Offset_ViewportWidth__ = 4,
Offset_ViewportHeight_ = 5,
}
const bufferValues = new Float32Array(Info.FloatsPerEntry);
const updateBufferValues = (canvasDevicePixelWidth: number = this._canvas.width, canvasDevicePixelHeight: number = this._canvas.height) => {
bufferValues[Info.Offset_CanvasWidth____] = canvasDevicePixelWidth;
bufferValues[Info.Offset_CanvasHeight___] = canvasDevicePixelHeight;
bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio);
bufferValues[Info.Offset_ViewportOffsetY] = 0;
bufferValues[Info.Offset_ViewportWidth__] = bufferValues[Info.Offset_CanvasWidth____] - bufferValues[Info.Offset_ViewportOffsetX];
bufferValues[Info.Offset_ViewportHeight_] = bufferValues[Info.Offset_CanvasHeight___] - bufferValues[Info.Offset_ViewportOffsetY];
return bufferValues;
};
layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer uniform buffer',
size: Info.BytesPerEntry,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
}, () => updateBufferValues())).object;
this._register(observeDevicePixelDimensions(this._canvas, getActiveWindow(), (w, h) => {
this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(w, h));
}));
}

const scrollOffsetBufferSize = 2;
this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer scroll offset buffer',
size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})).object;
this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);

// #endregion Uniforms

// #region Storage buffers

this._shapeBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer shape buffer',
size: this._shapeCollection.buffer.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})).object;

// #endregion Storage buffers

// #region Vertex buffer

this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, {
label: 'Monaco rectangle renderer vertex buffer',
size: quadVertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
}, quadVertices)).object;

// #endregion Vertex buffer

// #region Shader module

const module = this._device.createShaderModule({
label: 'Monaco rectangle renderer shader module',
code: rectangleRendererWgsl,
});

// #endregion Shader module

// #region Pipeline

this._pipeline = this._device.createRenderPipeline({
label: 'Monaco rectangle renderer render pipeline',
layout: 'auto',
vertex: {
module,
buffers: [
{
arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' }, // position
],
}
]
},
fragment: {
module,
targets: [
{
format: presentationFormat,
blend: {
color: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha'
},
alpha: {
srcFactor: 'src-alpha',
dstFactor: 'one-minus-src-alpha'
},
},
}
],
},
});

// #endregion Pipeline

// #region Bind group

this._bindGroup = this._device.createBindGroup({
label: 'Monaco rectangle renderer bind group',
layout: this._pipeline.getBindGroupLayout(0),
entries: [
{ binding: RectangleRendererBindingId.Shapes, resource: { buffer: this._shapeBindBuffer } },
{ binding: RectangleRendererBindingId.LayoutInfoUniform, resource: { buffer: layoutInfoUniformBuffer } },
{ binding: RectangleRendererBindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } },
],
});

// endregion Bind group

this._initialized = true;
}

register(x: number, y: number, width: number, height: number, red: number, green: number, blue: number, alpha: number): IObjectCollectionBufferEntry<RectangleRendererEntrySpec> {
// TODO: Expand buffer if needed
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return this._shapeCollection.createEntry({ x, y, width, height, red, green, blue, alpha });
}

// --- begin event handlers

public override onScrollChanged(e: ViewScrollChangedEvent): boolean {
this._scrollChanged = true;
return super.onScrollChanged(e);
}

// --- end event handlers

private _update() {
// TODO: Only write dirty range
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._device.queue.writeBuffer(this._shapeBindBuffer, 0, this._shapeCollection.buffer);

// Update scroll offset
if (this._scrollChanged) {
const dpr = getActiveWindow().devicePixelRatio;
this._scrollOffsetValueBuffer[0] = this._context.viewLayout.getCurrentScrollLeft() * dpr;
this._scrollOffsetValueBuffer[1] = this._context.viewLayout.getCurrentScrollTop() * dpr;
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer);
}
}

draw(viewportData: ViewportData) {
if (!this._initialized) {
return;
}

this._update();

const encoder = this._device.createCommandEncoder({ label: 'Monaco rectangle renderer command encoder' });

this._renderPassColorAttachment.view = this._ctx.getCurrentTexture().createView();
const pass = encoder.beginRenderPass(this._renderPassDescriptor);
pass.setPipeline(this._pipeline);
pass.setVertexBuffer(0, this._vertexBuffer);
pass.setBindGroup(0, this._bindGroup);

pass.draw(quadVertices.length / 2, this._shapeCollection.entryCount);
pass.end();

const commandBuffer = encoder.finish();
this._device.queue.submit([commandBuffer]);
}
}
Loading
Loading