Path: blob/main/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts
5240 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 {174if (this._store.isDisposed) {175return false;176}177const dpr = getActiveWindow().devicePixelRatio;178this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;179this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;180this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array<ArrayBuffer>);181return true;182}183184public override onThemeChanged(e: ViewThemeChangedEvent): boolean {185this._invalidateAllLines();186return true;187}188189public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean {190this._invalidateAllLines();191this._queueBufferUpdate(e);192return true;193}194195public override onZonesChanged(e: ViewZonesChangedEvent): boolean {196this._invalidateAllLines();197this._queueBufferUpdate(e);198199return true;200}201202// #endregion203204private _invalidateAllLines(): void {205this._upToDateLines[0].clear();206this._upToDateLines[1].clear();207}208209private _invalidateLinesFrom(lineNumber: number): void {210for (const i of [0, 1]) {211const upToDateLines = this._upToDateLines[i];212for (const upToDateLine of upToDateLines) {213if (upToDateLine >= lineNumber) {214upToDateLines.delete(upToDateLine);215}216}217}218}219220private _invalidateLineRange(fromLineNumber: number, toLineNumber: number): void {221for (let i = fromLineNumber; i <= toLineNumber; i++) {222this._upToDateLines[0].delete(i);223this._upToDateLines[1].delete(i);224}225}226227reset() {228this._invalidateAllLines();229for (const bufferIndex of [0, 1]) {230// Zero out buffer and upload to GPU to prevent stale rows from rendering231const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);232buffer.fill(0, 0, buffer.length);233this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);234}235this._finalRenderedLine = 0;236}237238update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number {239// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the240// loop. This is done so we don't need to trust the JIT compiler to do this optimization to241// avoid potential additional blocking time in garbage collector which is a common cause of242// dropped frames.243244let chars = '';245let segment: string | undefined;246let charWidth = 0;247let y = 0;248let x = 0;249let absoluteOffsetX = 0;250let absoluteOffsetY = 0;251let tabXOffset = 0;252let glyph: Readonly<ITextureAtlasPageGlyph>;253let cellIndex = 0;254255let tokenStartIndex = 0;256let tokenEndIndex = 0;257let tokenMetadata = 0;258259let decorationStyleSetBold: boolean | undefined;260let decorationStyleSetColor: number | undefined;261let decorationStyleSetOpacity: number | undefined;262let decorationStyleSetStrikethrough: boolean | undefined;263let decorationStyleSetStrikethroughThickness: number | undefined;264let decorationStyleSetStrikethroughColor: number | undefined;265266let lineData: ViewLineRenderingData;267let decoration: InlineDecoration;268let fillStartIndex = 0;269let fillEndIndex = 0;270271let tokens: IViewLineTokens;272273const dpr = getActiveWindow().devicePixelRatio;274let contentSegmenter: IContentSegmenter;275276if (!this._scrollInitialized) {277this.onScrollChanged();278this._scrollInitialized = true;279}280281// Update cell data282const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);283const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;284285const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex];286let dirtyLineStart = 3000;287let dirtyLineEnd = 0;288289// Handle any queued buffer updates290const queuedBufferUpdates = this._queuedBufferUpdates[this._activeDoubleBufferIndex];291while (queuedBufferUpdates.length) {292const e = queuedBufferUpdates.shift()!;293switch (e.type) {294// TODO: Refine these cases so we're not throwing away everything295case ViewEventType.ViewConfigurationChanged:296case ViewEventType.ViewLineMappingChanged:297case ViewEventType.ViewZonesChanged: {298cellBuffer.fill(0);299300dirtyLineStart = 1;301dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine);302this._finalRenderedLine = 0;303break;304}305case ViewEventType.ViewLinesDeleted: {306// Shift content below deleted line up307const deletedLineContentStartIndex = (e.fromLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;308const deletedLineContentEndIndex = (e.toLineNumber) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;309const nullContentStartIndex = (this._finalRenderedLine - (e.toLineNumber - e.fromLineNumber + 1)) * FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;310cellBuffer.set(cellBuffer.subarray(deletedLineContentEndIndex), deletedLineContentStartIndex);311312// Zero out content on lines that are no longer valid313cellBuffer.fill(0, nullContentStartIndex);314315// Update dirty lines and final rendered line316dirtyLineStart = Math.min(dirtyLineStart, e.fromLineNumber);317dirtyLineEnd = Math.max(dirtyLineEnd, this._finalRenderedLine);318this._finalRenderedLine -= e.toLineNumber - e.fromLineNumber + 1;319break;320}321}322}323324for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {325326// Only attempt to render lines that the GPU renderer can handle327if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {328fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;329fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;330cellBuffer.fill(0, fillStartIndex, fillEndIndex);331332dirtyLineStart = Math.min(dirtyLineStart, y);333dirtyLineEnd = Math.max(dirtyLineEnd, y);334335continue;336}337338// Skip updating the line if it's already up to date339if (upToDateLines.has(y)) {340continue;341}342343dirtyLineStart = Math.min(dirtyLineStart, y);344dirtyLineEnd = Math.max(dirtyLineEnd, y);345346lineData = viewportData.getViewLineRenderingData(y);347tabXOffset = 0;348349contentSegmenter = createContentSegmenter(lineData, viewLineOptions);350charWidth = viewLineOptions.spaceWidth * dpr;351absoluteOffsetX = (lineData.minColumn - 1) * charWidth;352353tokens = lineData.tokens;354tokenStartIndex = lineData.minColumn - 1;355tokenEndIndex = 0;356for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {357tokenEndIndex = tokens.getEndOffset(tokenIndex);358if (tokenEndIndex <= tokenStartIndex) {359// The faux indent part of the line should have no token type360continue;361}362363tokenMetadata = tokens.getMetadata(tokenIndex);364365for (x = tokenStartIndex; x < tokenEndIndex; x++) {366// Only render lines that do not exceed maximum columns367if (x > FullFileRenderStrategy.maxSupportedColumns) {368break;369}370segment = contentSegmenter.getSegmentAtIndex(x);371if (segment === undefined) {372continue;373}374chars = segment;375376if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {377charWidth = this.glyphRasterizer.getTextMetrics(chars).width;378}379380decorationStyleSetColor = undefined;381decorationStyleSetBold = undefined;382decorationStyleSetOpacity = undefined;383decorationStyleSetStrikethrough = undefined;384decorationStyleSetStrikethroughThickness = undefined;385decorationStyleSetStrikethroughColor = undefined;386387// Apply supported inline decoration styles to the cell metadata388for (decoration of lineData.inlineDecorations) {389// This is Range.strictContainsPosition except it works at the cell level,390// it's also inlined to avoid overhead.391if (392(y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||393(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||394(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)395) {396continue;397}398399const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);400for (const rule of rules) {401for (const r of rule.style) {402const value = rule.styleMap.get(r)?.toString() ?? '';403switch (r) {404case 'color': {405// TODO: This parsing and error handling should move into canRender so fallback406// to DOM works407const parsedColor = Color.Format.CSS.parse(value);408if (!parsedColor) {409throw new BugIndicatingError('Invalid color format ' + value);410}411decorationStyleSetColor = parsedColor.toNumber32Bit();412break;413}414case 'font-weight': {415const parsedValue = parseCssFontWeight(value);416if (parsedValue >= 400) {417decorationStyleSetBold = true;418// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)419} else {420decorationStyleSetBold = false;421// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)422}423break;424}425case 'opacity': {426const parsedValue = parseCssOpacity(value);427decorationStyleSetOpacity = parsedValue;428break;429}430case 'text-decoration':431case 'text-decoration-line': {432if (value === 'line-through') {433decorationStyleSetStrikethrough = true;434}435break;436}437case 'text-decoration-thickness': {438const match = value.match(/^(\d+(?:\.\d+)?)px$/);439if (match) {440decorationStyleSetStrikethroughThickness = parseFloat(match[1]);441}442break;443}444case 'text-decoration-color': {445let colorValue = value;446const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/);447if (varMatch) {448colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]);449}450const parsedColor = Color.Format.CSS.parse(colorValue);451if (parsedColor) {452decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit();453}454break;455}456case 'text-decoration-style': {457// These are validated in canRender and use default behavior458break;459}460default: throw new BugIndicatingError('Unexpected inline decoration style');461}462}463}464}465466if (chars === ' ' || chars === '\t') {467// Zero out glyph to ensure it doesn't get rendered468cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;469cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry);470// Adjust xOffset for tab stops471if (chars === '\t') {472// Find the pixel offset between the current position and the next tab stop473const offsetBefore = x + tabXOffset;474tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);475absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);476// Convert back to offset excluding x and the current character477tabXOffset -= x + 1;478} else {479absoluteOffsetX += charWidth;480}481continue;482}483484const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor);485glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);486487absoluteOffsetY = Math.round(488// Top of layout box (includes line height)489viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +490491// Delta from top of layout box (includes line height) to top of the inline box (no line height)492Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +493494// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn495// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical496// baseline.497glyph.fontBoundingBoxAscent498);499500cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;501cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);502cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;503cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;504cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;505506// Adjust the x pixel offset for the next character507absoluteOffsetX += charWidth;508}509510tokenStartIndex = tokenEndIndex;511}512513// Clear to end of line514fillStartIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell;515fillEndIndex = (y * FullFileRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;516cellBuffer.fill(0, fillStartIndex, fillEndIndex);517518upToDateLines.add(y);519}520521const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;522523// Only write when there is changed data524dirtyLineStart = Math.min(dirtyLineStart, FullFileRenderStrategy.maxSupportedLines);525dirtyLineEnd = Math.min(dirtyLineEnd, FullFileRenderStrategy.maxSupportedLines);526if (dirtyLineStart <= dirtyLineEnd) {527// Write buffer and swap it out to unblock writes528this._device.queue.writeBuffer(529this._cellBindBuffer,530(dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT,531cellBuffer.buffer,532(dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT,533(dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT534);535}536537this._finalRenderedLine = Math.max(this._finalRenderedLine, dirtyLineEnd);538539this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;540541this._visibleObjectCount = visibleObjectCount;542543return visibleObjectCount;544}545546draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {547if (this._visibleObjectCount <= 0) {548throw new BugIndicatingError('Attempt to draw 0 objects');549}550pass.draw(551quadVertices.length / 2,552this._visibleObjectCount,553undefined,554(viewportData.startLineNumber - 1) * FullFileRenderStrategy.maxSupportedColumns555);556}557558/**559* Queue updates that need to happen on the active buffer, not just the cache. This will be560* deferred to when the actual cell buffer is changed since the active buffer could be locked by561* the GPU which would block the main thread.562*/563private _queueBufferUpdate(e: QueuedBufferEvent) {564this._queuedBufferUpdates[0].push(e);565this._queuedBufferUpdates[1].push(e);566}567}568569function parseCssFontWeight(value: string) {570switch (value) {571case 'lighter':572case 'normal': return 400;573case 'bolder':574case 'bold': return 700;575}576return parseInt(value);577}578579function parseCssOpacity(value: string): number {580if (value.endsWith('%')) {581return parseFloat(value.substring(0, value.length - 1)) / 100;582}583if (value.match(/^\d+(?:\.\d*)/)) {584return parseFloat(value);585}586return 1;587}588589590