Path: blob/main/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.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 { 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;6768private _scrollOffsetBindBuffer: GPUBuffer;69private _scrollOffsetValueBuffer: Float32Array;70private _scrollInitialized: boolean = false;7172get bindGroupEntries(): GPUBindGroupEntry[] {73return [74{ binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } },75{ binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } }76];77}7879private readonly _onDidChangeBindGroupEntries = this._register(new Emitter<void>());80readonly onDidChangeBindGroupEntries = this._onDidChangeBindGroupEntries.event;8182constructor(83context: ViewContext,84viewGpuContext: ViewGpuContext,85device: GPUDevice,86glyphRasterizer: { value: GlyphRasterizer },87) {88super(context, viewGpuContext, device, glyphRasterizer);8990this._rebuildCellBuffer(this._cellBindBufferLineCapacity);9192const scrollOffsetBufferSize = 2;93this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {94label: 'Monaco scroll offset buffer',95size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,96usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,97})).object;98this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);99}100101private _rebuildCellBuffer(lineCount: number) {102this._cellBindBuffer?.destroy();103104// Increase in chunks so resizing a window by hand doesn't keep allocating and throwing away105const lineCountWithIncrement = (Math.floor(lineCount / Constants.CellBindBufferCapacityIncrement) + 1) * Constants.CellBindBufferCapacityIncrement;106107const bufferSize = lineCountWithIncrement * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT;108this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {109label: 'Monaco full file cell buffer',110size: bufferSize,111usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,112})).object;113this._cellValueBuffers = [114new ArrayBuffer(bufferSize),115new ArrayBuffer(bufferSize),116];117this._cellBindBufferLineCapacity = lineCountWithIncrement;118119this._onDidChangeBindGroupEntries.fire();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 {133return true;134}135136public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {137return true;138}139140public override onTokensChanged(e: ViewTokensChangedEvent): boolean {141return true;142}143144public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {145return true;146}147148public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {149return true;150}151152public override onLinesChanged(e: ViewLinesChangedEvent): boolean {153return true;154}155156public override onScrollChanged(e?: ViewScrollChangedEvent): boolean {157const dpr = getActiveWindow().devicePixelRatio;158this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;159this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;160this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array<ArrayBuffer>);161return true;162}163164public override onThemeChanged(e: ViewThemeChangedEvent): boolean {165return true;166}167168public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean {169return true;170}171172public override onZonesChanged(e: ViewZonesChangedEvent): boolean {173return true;174}175176// #endregion177178reset() {179for (const bufferIndex of [0, 1]) {180// Zero out buffer and upload to GPU to prevent stale rows from rendering181const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);182buffer.fill(0, 0, buffer.length);183this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);184}185}186187update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number {188// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the189// loop. This is done so we don't need to trust the JIT compiler to do this optimization to190// avoid potential additional blocking time in garbage collector which is a common cause of191// dropped frames.192193let chars = '';194let segment: string | undefined;195let charWidth = 0;196let y = 0;197let x = 0;198let absoluteOffsetX = 0;199let absoluteOffsetY = 0;200let tabXOffset = 0;201let glyph: Readonly<ITextureAtlasPageGlyph>;202let cellIndex = 0;203204let tokenStartIndex = 0;205let tokenEndIndex = 0;206let tokenMetadata = 0;207208let decorationStyleSetBold: boolean | undefined;209let decorationStyleSetColor: number | undefined;210let decorationStyleSetOpacity: number | undefined;211212let lineData: ViewLineRenderingData;213let decoration: InlineDecoration;214let fillStartIndex = 0;215let fillEndIndex = 0;216217let tokens: IViewLineTokens;218219const dpr = getActiveWindow().devicePixelRatio;220let contentSegmenter: IContentSegmenter;221222if (!this._scrollInitialized) {223this.onScrollChanged();224this._scrollInitialized = true;225}226227// Zero out cell buffer or rebuild if needed228if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) {229this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1);230}231const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);232cellBuffer.fill(0);233234const lineIndexCount = ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;235236for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {237238// Only attempt to render lines that the GPU renderer can handle239if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {240continue;241}242243lineData = viewportData.getViewLineRenderingData(y);244tabXOffset = 0;245246contentSegmenter = createContentSegmenter(lineData, viewLineOptions);247charWidth = viewLineOptions.spaceWidth * dpr;248absoluteOffsetX = 0;249250tokens = lineData.tokens;251tokenStartIndex = lineData.minColumn - 1;252tokenEndIndex = 0;253for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {254tokenEndIndex = tokens.getEndOffset(tokenIndex);255if (tokenEndIndex <= tokenStartIndex) {256// The faux indent part of the line should have no token type257continue;258}259260tokenMetadata = tokens.getMetadata(tokenIndex);261262for (x = tokenStartIndex; x < tokenEndIndex; x++) {263// Only render lines that do not exceed maximum columns264if (x > ViewportRenderStrategy.maxSupportedColumns) {265break;266}267segment = contentSegmenter.getSegmentAtIndex(x);268if (segment === undefined) {269continue;270}271chars = segment;272273if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {274charWidth = this.glyphRasterizer.getTextMetrics(chars).width;275}276277decorationStyleSetColor = undefined;278decorationStyleSetBold = undefined;279decorationStyleSetOpacity = undefined;280281// Apply supported inline decoration styles to the cell metadata282for (decoration of lineData.inlineDecorations) {283// This is Range.strictContainsPosition except it works at the cell level,284// it's also inlined to avoid overhead.285if (286(y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||287(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||288(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)289) {290continue;291}292293const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);294for (const rule of rules) {295for (const r of rule.style) {296const value = rule.styleMap.get(r)?.toString() ?? '';297switch (r) {298case 'color': {299// TODO: This parsing and error handling should move into canRender so fallback300// to DOM works301const parsedColor = Color.Format.CSS.parse(value);302if (!parsedColor) {303throw new BugIndicatingError('Invalid color format ' + value);304}305decorationStyleSetColor = parsedColor.toNumber32Bit();306break;307}308case 'font-weight': {309const parsedValue = parseCssFontWeight(value);310if (parsedValue >= 400) {311decorationStyleSetBold = true;312// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)313} else {314decorationStyleSetBold = false;315// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)316}317break;318}319case 'opacity': {320const parsedValue = parseCssOpacity(value);321decorationStyleSetOpacity = parsedValue;322break;323}324default: throw new BugIndicatingError('Unexpected inline decoration style');325}326}327}328}329330if (chars === ' ' || chars === '\t') {331// Zero out glyph to ensure it doesn't get rendered332cellIndex = ((y - 1) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;333cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry);334// Adjust xOffset for tab stops335if (chars === '\t') {336// Find the pixel offset between the current position and the next tab stop337const offsetBefore = x + tabXOffset;338tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);339absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);340// Convert back to offset excluding x and the current character341tabXOffset -= x + 1;342} else {343absoluteOffsetX += charWidth;344}345continue;346}347348const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity);349glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);350351absoluteOffsetY = Math.round(352// Top of layout box (includes line height)353viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +354355// Delta from top of layout box (includes line height) to top of the inline box (no line height)356Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +357358// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn359// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical360// baseline.361glyph.fontBoundingBoxAscent362);363364cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;365cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);366cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;367cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;368cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;369370// Adjust the x pixel offset for the next character371absoluteOffsetX += charWidth;372}373374tokenStartIndex = tokenEndIndex;375}376377// Clear to end of line378fillStartIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell;379fillEndIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;380cellBuffer.fill(0, fillStartIndex, fillEndIndex);381}382383const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;384385// This render strategy always uploads the whole viewport386this._device.queue.writeBuffer(387this._cellBindBuffer,3880,389cellBuffer.buffer,3900,391(viewportData.endLineNumber - viewportData.startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT392);393394this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;395396this._visibleObjectCount = visibleObjectCount;397398return visibleObjectCount;399}400401draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {402if (this._visibleObjectCount <= 0) {403throw new BugIndicatingError('Attempt to draw 0 objects');404}405pass.draw(quadVertices.length / 2, this._visibleObjectCount);406}407}408409function parseCssFontWeight(value: string) {410switch (value) {411case 'lighter':412case 'normal': return 400;413case 'bolder':414case 'bold': return 700;415}416return parseInt(value);417}418419function parseCssOpacity(value: string): number {420if (value.endsWith('%')) {421return parseFloat(value.substring(0, value.length - 1)) / 100;422}423if (value.match(/^\d+(?:\.\d*)/)) {424return parseFloat(value);425}426return 1;427}428429430