Path: blob/main/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getActiveWindow } from '../../../../base/browser/dom.js';6import { Color } from '../../../../base/common/color.js';7import { BugIndicatingError } from '../../../../base/common/errors.js';8import { Emitter } from '../../../../base/common/event.js';9import { CursorColumns } from '../../../common/core/cursorColumns.js';10import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js';11import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js';12import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';13import type { ViewLineRenderingData } from '../../../common/viewModel.js';14import { InlineDecoration } from '../../../common/viewModel/inlineDecorations.js';15import type { ViewContext } from '../../../common/viewModel/viewContext.js';16import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js';17import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js';18import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js';19import { BindingId } from '../gpu.js';20import { GPULifecycle } from '../gpuDisposable.js';21import { quadVertices } from '../gpuUtils.js';22import { GlyphRasterizer } from '../raster/glyphRasterizer.js';23import { ViewGpuContext } from '../viewGpuContext.js';24import { BaseRenderStrategy } from './baseRenderStrategy.js';25import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';2627const enum Constants {28IndicesPerCell = 6,29CellBindBufferCapacityIncrement = 32,30CellBindBufferInitialCapacity = 63, // Will be rounded up to nearest increment31}3233const enum CellBufferInfo {34FloatsPerEntry = 6,35BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4,36Offset_X = 0,37Offset_Y = 1,38Offset_Unused1 = 2,39Offset_Unused2 = 3,40GlyphIndex = 4,41TextureIndex = 5,42}4344/**45* A render strategy that uploads the content of the entire viewport every frame.46*/47export class ViewportRenderStrategy extends BaseRenderStrategy {48/**49* The hard cap for line columns that can be rendered by the GPU renderer.50*/51static readonly maxSupportedColumns = 2000;5253readonly type = 'viewport';54readonly wgsl: string = fullFileRenderStrategyWgsl;5556private _cellBindBufferLineCapacity = Constants.CellBindBufferInitialCapacity;57private _cellBindBuffer!: GPUBuffer;5859/**60* The cell value buffers, these hold the cells and their glyphs. It's double buffers such that61* the thread doesn't block when one is being uploaded to the GPU.62*/63private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer];64private _activeDoubleBufferIndex: 0 | 1 = 0;6566private _visibleObjectCount: number = 0;67private _lastViewportLineCount: number = 0;6869private _scrollOffsetBindBuffer: GPUBuffer;70private _scrollOffsetValueBuffer: Float32Array;71private _scrollInitialized: boolean = false;7273get bindGroupEntries(): GPUBindGroupEntry[] {74return [75{ binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } },76{ binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } }77];78}7980private readonly _onDidChangeBindGroupEntries = this._register(new Emitter<void>());81readonly onDidChangeBindGroupEntries = this._onDidChangeBindGroupEntries.event;8283constructor(84context: ViewContext,85viewGpuContext: ViewGpuContext,86device: GPUDevice,87glyphRasterizer: { value: GlyphRasterizer },88) {89super(context, viewGpuContext, device, glyphRasterizer);9091this._rebuildCellBuffer(this._cellBindBufferLineCapacity);9293const scrollOffsetBufferSize = 2;94this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {95label: 'Monaco scroll offset buffer',96size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,97usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,98})).object;99this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);100}101102private _rebuildCellBuffer(lineCount: number) {103this._cellBindBuffer?.destroy();104105// Increase in chunks so resizing a window by hand doesn't keep allocating and throwing away106const lineCountWithIncrement = (Math.floor(lineCount / Constants.CellBindBufferCapacityIncrement) + 1) * Constants.CellBindBufferCapacityIncrement;107108const bufferSize = lineCountWithIncrement * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT;109this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {110label: 'Monaco full file cell buffer',111size: bufferSize,112usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,113})).object;114this._cellValueBuffers = [115new ArrayBuffer(bufferSize),116new ArrayBuffer(bufferSize),117];118this._cellBindBufferLineCapacity = lineCountWithIncrement;119this._lastViewportLineCount = 0;120121this._onDidChangeBindGroupEntries.fire();122}123124// #region Event handlers125126// The primary job of these handlers is to:127// 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when128// it's _within the viewport_.129// 2. Pass relevant events on to the render function so it can force certain line ranges to be130// re-rendered even if they're not in the viewport. For example when a view zone is added,131// there are lines that used to be visible but are no longer, so those ranges must be132// cleared and uploaded to the GPU.133134public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean {135return true;136}137138public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {139return true;140}141142public override onTokensChanged(e: ViewTokensChangedEvent): boolean {143return true;144}145146public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {147return true;148}149150public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {151return true;152}153154public override onLinesChanged(e: ViewLinesChangedEvent): boolean {155return true;156}157158public override onScrollChanged(e?: ViewScrollChangedEvent): boolean {159if (this._store.isDisposed) {160return false;161}162const dpr = getActiveWindow().devicePixelRatio;163this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;164this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;165this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array<ArrayBuffer>);166return true;167}168169public override onThemeChanged(e: ViewThemeChangedEvent): boolean {170return true;171}172173public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean {174return true;175}176177public override onZonesChanged(e: ViewZonesChangedEvent): boolean {178return true;179}180181// #endregion182183reset() {184for (const bufferIndex of [0, 1]) {185// Zero out buffer and upload to GPU to prevent stale rows from rendering186const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);187buffer.fill(0, 0, buffer.length);188this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);189}190this._lastViewportLineCount = 0;191}192193update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number {194// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the195// loop. This is done so we don't need to trust the JIT compiler to do this optimization to196// avoid potential additional blocking time in garbage collector which is a common cause of197// dropped frames.198199let chars = '';200let segment: string | undefined;201let charWidth = 0;202let y = 0;203let x = 0;204let absoluteOffsetX = 0;205let absoluteOffsetY = 0;206let tabXOffset = 0;207let glyph: Readonly<ITextureAtlasPageGlyph>;208let cellIndex = 0;209210let tokenStartIndex = 0;211let tokenEndIndex = 0;212let tokenMetadata = 0;213214let decorationStyleSetBold: boolean | undefined;215let decorationStyleSetColor: number | undefined;216let decorationStyleSetOpacity: number | undefined;217let decorationStyleSetStrikethrough: boolean | undefined;218let decorationStyleSetStrikethroughThickness: number | undefined;219let decorationStyleSetStrikethroughColor: number | undefined;220221let lineData: ViewLineRenderingData;222let decoration: InlineDecoration;223let fillStartIndex = 0;224let fillEndIndex = 0;225226let tokens: IViewLineTokens;227228const dpr = getActiveWindow().devicePixelRatio;229let contentSegmenter: IContentSegmenter;230231if (!this._scrollInitialized) {232this.onScrollChanged();233this._scrollInitialized = true;234}235236// Zero out cell buffer or rebuild if needed237if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) {238this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1);239}240const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);241cellBuffer.fill(0);242243const lineIndexCount = ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;244245for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {246247// Only attempt to render lines that the GPU renderer can handle248if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {249continue;250}251252lineData = viewportData.getViewLineRenderingData(y);253tabXOffset = 0;254255contentSegmenter = createContentSegmenter(lineData, viewLineOptions);256charWidth = viewLineOptions.spaceWidth * dpr;257absoluteOffsetX = (lineData.minColumn - 1) * charWidth;258259tokens = lineData.tokens;260tokenStartIndex = lineData.minColumn - 1;261tokenEndIndex = 0;262for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {263tokenEndIndex = tokens.getEndOffset(tokenIndex);264if (tokenEndIndex <= tokenStartIndex) {265// The faux indent part of the line should have no token type266continue;267}268269tokenMetadata = tokens.getMetadata(tokenIndex);270271for (x = tokenStartIndex; x < tokenEndIndex; x++) {272// Only render lines that do not exceed maximum columns273if (x > ViewportRenderStrategy.maxSupportedColumns) {274break;275}276segment = contentSegmenter.getSegmentAtIndex(x);277if (segment === undefined) {278continue;279}280chars = segment;281282if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {283charWidth = this.glyphRasterizer.getTextMetrics(chars).width;284}285286decorationStyleSetColor = undefined;287decorationStyleSetBold = undefined;288decorationStyleSetOpacity = undefined;289decorationStyleSetStrikethrough = undefined;290decorationStyleSetStrikethroughThickness = undefined;291decorationStyleSetStrikethroughColor = undefined;292293// Apply supported inline decoration styles to the cell metadata294for (decoration of lineData.inlineDecorations) {295// This is Range.strictContainsPosition except it works at the cell level,296// it's also inlined to avoid overhead.297if (298(y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||299(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||300(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)301) {302continue;303}304305const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);306for (const rule of rules) {307for (const r of rule.style) {308const value = rule.styleMap.get(r)?.toString() ?? '';309switch (r) {310case 'color': {311// TODO: This parsing and error handling should move into canRender so fallback312// to DOM works313const parsedColor = Color.Format.CSS.parse(value);314if (!parsedColor) {315throw new BugIndicatingError('Invalid color format ' + value);316}317decorationStyleSetColor = parsedColor.toNumber32Bit();318break;319}320case 'font-weight': {321const parsedValue = parseCssFontWeight(value);322if (parsedValue >= 400) {323decorationStyleSetBold = true;324// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)325} else {326decorationStyleSetBold = false;327// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)328}329break;330}331case 'opacity': {332const parsedValue = parseCssOpacity(value);333decorationStyleSetOpacity = parsedValue;334break;335}336case 'text-decoration':337case 'text-decoration-line': {338if (value === 'line-through') {339decorationStyleSetStrikethrough = true;340}341break;342}343case 'text-decoration-thickness': {344const match = value.match(/^(\d+(?:\.\d+)?)px$/);345if (match) {346decorationStyleSetStrikethroughThickness = parseFloat(match[1]);347}348break;349}350case 'text-decoration-color': {351let colorValue = value;352const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/);353if (varMatch) {354colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]);355}356const parsedColor = Color.Format.CSS.parse(colorValue);357if (parsedColor) {358decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit();359}360break;361}362case 'text-decoration-style': {363// These are validated in canRender and use default behavior364break;365}366default: throw new BugIndicatingError('Unexpected inline decoration style');367}368}369}370}371372if (chars === ' ' || chars === '\t') {373// Zero out glyph to ensure it doesn't get rendered374cellIndex = ((y - 1) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;375cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry);376// Adjust xOffset for tab stops377if (chars === '\t') {378// Find the pixel offset between the current position and the next tab stop379const offsetBefore = x + tabXOffset;380tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);381absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);382// Convert back to offset excluding x and the current character383tabXOffset -= x + 1;384} else {385absoluteOffsetX += charWidth;386}387continue;388}389390const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor);391glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);392393absoluteOffsetY = Math.round(394// Top of layout box (includes line height)395viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +396397// Delta from top of layout box (includes line height) to top of the inline box (no line height)398Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +399400// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn401// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical402// baseline.403glyph.fontBoundingBoxAscent404);405406cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;407cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);408cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;409cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;410cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;411412// Adjust the x pixel offset for the next character413absoluteOffsetX += charWidth;414}415416tokenStartIndex = tokenEndIndex;417}418419// Clear to end of line420fillStartIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell;421fillEndIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;422cellBuffer.fill(0, fillStartIndex, fillEndIndex);423}424425const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;426const viewportLineCount = viewportData.endLineNumber - viewportData.startLineNumber + 1;427428// This render strategy always uploads the whole viewport429this._device.queue.writeBuffer(430this._cellBindBuffer,4310,432cellBuffer.buffer,4330,434visibleObjectCount * Float32Array.BYTES_PER_ELEMENT435);436437// Clear stale lines in GPU buffer if viewport shrunk438if (viewportLineCount < this._lastViewportLineCount) {439const staleLineCount = this._lastViewportLineCount - viewportLineCount;440const staleStartOffset = visibleObjectCount * Float32Array.BYTES_PER_ELEMENT;441const staleByteCount = staleLineCount * lineIndexCount * Float32Array.BYTES_PER_ELEMENT;442// Write zeros from the zeroed cellBuffer for the stale region443this._device.queue.writeBuffer(444this._cellBindBuffer,445staleStartOffset,446cellBuffer.buffer,447visibleObjectCount * Float32Array.BYTES_PER_ELEMENT,448staleByteCount449);450}451this._lastViewportLineCount = viewportLineCount;452453this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;454455this._visibleObjectCount = visibleObjectCount;456457return visibleObjectCount;458}459460draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {461if (this._visibleObjectCount <= 0) {462throw new BugIndicatingError('Attempt to draw 0 objects');463}464pass.draw(quadVertices.length / 2, this._visibleObjectCount);465}466}467468function parseCssFontWeight(value: string) {469switch (value) {470case 'lighter':471case 'normal': return 400;472case 'bolder':473case 'bold': return 700;474}475return parseInt(value);476}477478function parseCssOpacity(value: string): number {479if (value.endsWith('%')) {480return parseFloat(value.substring(0, value.length - 1)) / 100;481}482if (value.match(/^\d+(?:\.\d*)/)) {483return parseFloat(value);484}485return 1;486}487488489