Path: blob/main/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts
3296 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 { CursorColumns } from '../../../common/core/cursorColumns.js';9import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js';10import { ViewEventType, type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js';11import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';12import type { ViewLineRenderingData } from '../../../common/viewModel.js';13import type { ViewContext } from '../../../common/viewModel/viewContext.js';14import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js';15import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js';16import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js';17import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';18import { BindingId } from '../gpu.js';19import { GPULifecycle } from '../gpuDisposable.js';20import { quadVertices } from '../gpuUtils.js';21import { GlyphRasterizer } from '../raster/glyphRasterizer.js';22import { ViewGpuContext } from '../viewGpuContext.js';23import { BaseRenderStrategy } from './baseRenderStrategy.js';24import { InlineDecoration } from '../../../common/viewModel/inlineDecorations.js';2526const enum Constants {27IndicesPerCell = 6,28}2930const enum CellBufferInfo {31FloatsPerEntry = 6,32BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4,33Offset_X = 0,34Offset_Y = 1,35Offset_Unused1 = 2,36Offset_Unused2 = 3,37GlyphIndex = 4,38TextureIndex = 5,39}4041type QueuedBufferEvent = (42ViewConfigurationChangedEvent |43ViewLineMappingChangedEvent |44ViewLinesDeletedEvent |45ViewZonesChangedEvent46);4748/**49* A render strategy that tracks a large buffer, uploading only dirty lines as they change and50* leveraging heavy caching. This is the most performant strategy but has limitations around long51* lines and too many lines.52*/53export class FullFileRenderStrategy extends BaseRenderStrategy {5455/**56* The hard cap for line count that can be rendered by the GPU renderer.57*/58static readonly maxSupportedLines = 3000;5960/**61* The hard cap for line columns that can be rendered by the GPU renderer.62*/63static readonly maxSupportedColumns = 200;6465readonly type = 'fullfile';66readonly wgsl: string = fullFileRenderStrategyWgsl;6768private _cellBindBuffer!: GPUBuffer;6970/**71* The cell value buffers, these hold the cells and their glyphs. It's double buffers such that72* the thread doesn't block when one is being uploaded to the GPU.73*/74private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer];75private _activeDoubleBufferIndex: 0 | 1 = 0;7677private readonly _upToDateLines: [Set<number>, Set<number>] = [new Set(), new Set()];78private _visibleObjectCount: number = 0;79private _finalRenderedLine: number = 0;8081private _scrollOffsetBindBuffer: GPUBuffer;82private _scrollOffsetValueBuffer: Float32Array;83private _scrollInitialized: boolean = false;8485private readonly _queuedBufferUpdates: [QueuedBufferEvent[], QueuedBufferEvent[]] = [[], []];8687get bindGroupEntries(): GPUBindGroupEntry[] {88return [89{ binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } },90{ binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } }91];92}9394constructor(95context: ViewContext,96viewGpuContext: ViewGpuContext,97device: GPUDevice,98glyphRasterizer: { value: GlyphRasterizer },99) {100super(context, viewGpuContext, device, glyphRasterizer);101102const bufferSize = FullFileRenderStrategy.maxSupportedLines * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT;103this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {104label: 'Monaco full file cell buffer',105size: bufferSize,106usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,107})).object;108this._cellValueBuffers = [109new ArrayBuffer(bufferSize),110new ArrayBuffer(bufferSize),111];112113const scrollOffsetBufferSize = 2;114this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {115label: 'Monaco scroll offset buffer',116size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,117usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,118})).object;119this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);120}121122// #region Event handlers123124// The primary job of these handlers is to:125// 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when126// it's _within the viewport_.127// 2. Pass relevant events on to the render function so it can force certain line ranges to be128// re-rendered even if they're not in the viewport. For example when a view zone is added,129// there are lines that used to be visible but are no longer, so those ranges must be130// cleared and uploaded to the GPU.131132public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean {133this._invalidateAllLines();134this._queueBufferUpdate(e);135return true;136}137138public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {139this._invalidateAllLines();140return true;141}142143public override onTokensChanged(e: ViewTokensChangedEvent): boolean {144// TODO: This currently fires for the entire viewport whenever scrolling stops145// https://github.com/microsoft/vscode/issues/233942146for (const range of e.ranges) {147this._invalidateLineRange(range.fromLineNumber, range.toLineNumber);148}149return true;150}151152public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {153// TODO: This currently invalidates everything after the deleted line, it could shift the154// line data up to retain some up to date lines155// TODO: This does not invalidate lines that are no longer in the file156this._invalidateLinesFrom(e.fromLineNumber);157this._queueBufferUpdate(e);158return true;159}160161public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {162// TODO: This currently invalidates everything after the deleted line, it could shift the163// line data up to retain some up to date lines164this._invalidateLinesFrom(e.fromLineNumber);165return true;166}167168public override onLinesChanged(e: ViewLinesChangedEvent): boolean {169this._invalidateLineRange(e.fromLineNumber, e.fromLineNumber + e.count);170return true;171}172173public override onScrollChanged(e?: ViewScrollChangedEvent): boolean {174const dpr = getActiveWindow().devicePixelRatio;175this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;176this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;177this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array<ArrayBuffer>);178return true;179}180181public override onThemeChanged(e: ViewThemeChangedEvent): boolean {182this._invalidateAllLines();183return true;184}185186public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean {187this._invalidateAllLines();188this._queueBufferUpdate(e);189return true;190}191192public override onZonesChanged(e: ViewZonesChangedEvent): boolean {193this._invalidateAllLines();194this._queueBufferUpdate(e);195196return true;197}198199// #endregion200201private _invalidateAllLines(): void {202this._upToDateLines[0].clear();203this._upToDateLines[1].clear();204}205206private _invalidateLinesFrom(lineNumber: number): void {207for (const i of [0, 1]) {208const upToDateLines = this._upToDateLines[i];209for (const upToDateLine of upToDateLines) {210if (upToDateLine >= lineNumber) {211upToDateLines.delete(upToDateLine);212}213}214}215}216217private _invalidateLineRange(fromLineNumber: number, toLineNumber: number): void {218for (let i = fromLineNumber; i <= toLineNumber; i++) {219this._upToDateLines[0].delete(i);220this._upToDateLines[1].delete(i);221}222}223224reset() {225this._invalidateAllLines();226for (const bufferIndex of [0, 1]) {227// Zero out buffer and upload to GPU to prevent stale rows from rendering228const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);229buffer.fill(0, 0, buffer.length);230this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);231}232this._finalRenderedLine = 0;233}234235update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number {236// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the237// loop. This is done so we don't need to trust the JIT compiler to do this optimization to238// avoid potential additional blocking time in garbage collector which is a common cause of239// dropped frames.240241let chars = '';242let segment: string | undefined;243let charWidth = 0;244let y = 0;245let x = 0;246let absoluteOffsetX = 0;247let absoluteOffsetY = 0;248let tabXOffset = 0;249let glyph: Readonly<ITextureAtlasPageGlyph>;250let cellIndex = 0;251252let tokenStartIndex = 0;253let tokenEndIndex = 0;254let tokenMetadata = 0;255256let decorationStyleSetBold: boolean | undefined;257let decorationStyleSetColor: number | undefined;258let decorationStyleSetOpacity: number | undefined;259260let lineData: ViewLineRenderingData;261let decoration: InlineDecoration;262let fillStartIndex = 0;263let fillEndIndex = 0;264265let tokens: IViewLineTokens;266267const dpr = getActiveWindow().devicePixelRatio;268let contentSegmenter: IContentSegmenter;269270if (!this._scrollInitialized) {271this.onScrollChanged();272this._scrollInitialized = true;273}274275// Update cell data276const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);277const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;278279const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex];280let dirtyLineStart = 3000;281let dirtyLineEnd = 0;282283// Handle any queued buffer updates284const queuedBufferUpdates = this._queuedBufferUpdates[this._activeDoubleBufferIndex];285while (queuedBufferUpdates.length) {286const e = queuedBufferUpdates.shift()!;287switch (e.type) {288// TODO: Refine these cases so we're not throwing away everything289case ViewEventType.ViewConfigurationChanged:290case ViewEventType.ViewLineMappingChanged:291case ViewEventType.ViewZonesChanged: {292cellBuffer.fill(0);293294dirtyLineStart = 1;295dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine);296this._finalRenderedLine = 0;297break;298}299case ViewEventType.ViewLinesDeleted: {300// Shift content below deleted line up301const deletedLineContentStartIndex = (e.fromLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;302const deletedLineContentEndIndex = (e.toLineNumber) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;303const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;304cellBuffer.set(cellBuffer.subarray(deletedLineContentEndIndex), deletedLineContentStartIndex);305306// Zero out content on lines that are no longer valid307cellBuffer.fill(0, nullContentStartIndex);308309// Update dirty lines and final rendered line310dirtyLineStart = Math.min(dirtyLineStart, e.fromLineNumber);311dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine);312this._finalRenderedLine -= e.toLineNumber - e.fromLineNumber + 1;313break;314}315}316}317318for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {319320// Only attempt to render lines that the GPU renderer can handle321if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {322fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;323fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;324cellBuffer.fill(0, fillStartIndex, fillEndIndex);325326dirtyLineStart = Math.min(dirtyLineStart, y);327dirtyLineEnd = Math.max(dirtyLineEnd, y);328329continue;330}331332// Skip updating the line if it's already up to date333if (upToDateLines.has(y)) {334continue;335}336337dirtyLineStart = Math.min(dirtyLineStart, y);338dirtyLineEnd = Math.max(dirtyLineEnd, y);339340lineData = viewportData.getViewLineRenderingData(y);341tabXOffset = 0;342343contentSegmenter = createContentSegmenter(lineData, viewLineOptions);344charWidth = viewLineOptions.spaceWidth * dpr;345absoluteOffsetX = 0;346347tokens = lineData.tokens;348tokenStartIndex = lineData.minColumn - 1;349tokenEndIndex = 0;350for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {351tokenEndIndex = tokens.getEndOffset(tokenIndex);352if (tokenEndIndex <= tokenStartIndex) {353// The faux indent part of the line should have no token type354continue;355}356357tokenMetadata = tokens.getMetadata(tokenIndex);358359for (x = tokenStartIndex; x < tokenEndIndex; x++) {360// Only render lines that do not exceed maximum columns361if (x > FullFileRenderStrategy.maxSupportedColumns) {362break;363}364segment = contentSegmenter.getSegmentAtIndex(x);365if (segment === undefined) {366continue;367}368chars = segment;369370if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {371charWidth = this.glyphRasterizer.getTextMetrics(chars).width;372}373374decorationStyleSetColor = undefined;375decorationStyleSetBold = undefined;376decorationStyleSetOpacity = undefined;377378// Apply supported inline decoration styles to the cell metadata379for (decoration of lineData.inlineDecorations) {380// This is Range.strictContainsPosition except it works at the cell level,381// it's also inlined to avoid overhead.382if (383(y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||384(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||385(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)386) {387continue;388}389390const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);391for (const rule of rules) {392for (const r of rule.style) {393const value = rule.styleMap.get(r)?.toString() ?? '';394switch (r) {395case 'color': {396// TODO: This parsing and error handling should move into canRender so fallback397// to DOM works398const parsedColor = Color.Format.CSS.parse(value);399if (!parsedColor) {400throw new BugIndicatingError('Invalid color format ' + value);401}402decorationStyleSetColor = parsedColor.toNumber32Bit();403break;404}405case 'font-weight': {406const parsedValue = parseCssFontWeight(value);407if (parsedValue >= 400) {408decorationStyleSetBold = true;409// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)410} else {411decorationStyleSetBold = false;412// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)413}414break;415}416case 'opacity': {417const parsedValue = parseCssOpacity(value);418decorationStyleSetOpacity = parsedValue;419break;420}421default: throw new BugIndicatingError('Unexpected inline decoration style');422}423}424}425}426427if (chars === ' ' || chars === '\t') {428// Zero out glyph to ensure it doesn't get rendered429cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;430cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry);431// Adjust xOffset for tab stops432if (chars === '\t') {433// Find the pixel offset between the current position and the next tab stop434const offsetBefore = x + tabXOffset;435tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);436absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);437// Convert back to offset excluding x and the current character438tabXOffset -= x + 1;439} else {440absoluteOffsetX += charWidth;441}442continue;443}444445const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity);446glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);447448absoluteOffsetY = Math.round(449// Top of layout box (includes line height)450viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +451452// Delta from top of layout box (includes line height) to top of the inline box (no line height)453Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +454455// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn456// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical457// baseline.458glyph.fontBoundingBoxAscent459);460461cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;462cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);463cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;464cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;465cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;466467// Adjust the x pixel offset for the next character468absoluteOffsetX += charWidth;469}470471tokenStartIndex = tokenEndIndex;472}473474// Clear to end of line475fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell;476fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;477cellBuffer.fill(0, fillStartIndex, fillEndIndex);478479upToDateLines.add(y);480}481482const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;483484// Only write when there is changed data485dirtyLineStart = Math.min(dirtyLineStart, FullFileRenderStrategy.maxSupportedLines);486dirtyLineEnd = Math.min(dirtyLineEnd, FullFileRenderStrategy.maxSupportedLines);487if (dirtyLineStart <= dirtyLineEnd) {488// Write buffer and swap it out to unblock writes489this._device.queue.writeBuffer(490this._cellBindBuffer,491(dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT,492cellBuffer.buffer,493(dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT,494(dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT495);496}497498this._finalRenderedLine = Math.max(this._finalRenderedLine, dirtyLineEnd);499500this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;501502this._visibleObjectCount = visibleObjectCount;503504return visibleObjectCount;505}506507draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {508if (this._visibleObjectCount <= 0) {509throw new BugIndicatingError('Attempt to draw 0 objects');510}511pass.draw(512quadVertices.length / 2,513this._visibleObjectCount,514undefined,515(viewportData.startLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns516);517}518519/**520* Queue updates that need to happen on the active buffer, not just the cache. This will be521* deferred to when the actual cell buffer is changed since the active buffer could be locked by522* the GPU which would block the main thread.523*/524private _queueBufferUpdate(e: QueuedBufferEvent) {525this._queuedBufferUpdates[0].push(e);526this._queuedBufferUpdates[1].push(e);527}528}529530function parseCssFontWeight(value: string) {531switch (value) {532case 'lighter':533case 'normal': return 400;534case 'bolder':535case 'bold': return 700;536}537return parseInt(value);538}539540function parseCssOpacity(value: string): number {541if (value.endsWith('%')) {542return parseFloat(value.substring(0, value.length - 1)) / 100;543}544if (value.match(/^\d+(?:\.\d*)/)) {545return parseFloat(value);546}547return 1;548}549550551