Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts
5221 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { getActiveWindow } from '../../../../base/browser/dom.js';
7
import { Color } from '../../../../base/common/color.js';
8
import { BugIndicatingError } from '../../../../base/common/errors.js';
9
import { Emitter } from '../../../../base/common/event.js';
10
import { CursorColumns } from '../../../common/core/cursorColumns.js';
11
import type { IViewLineTokens } from '../../../common/tokens/lineTokens.js';
12
import { type ViewConfigurationChangedEvent, type ViewDecorationsChangedEvent, type ViewLineMappingChangedEvent, type ViewLinesChangedEvent, type ViewLinesDeletedEvent, type ViewLinesInsertedEvent, type ViewScrollChangedEvent, type ViewThemeChangedEvent, type ViewTokensChangedEvent, type ViewZonesChangedEvent } from '../../../common/viewEvents.js';
13
import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js';
14
import type { ViewLineRenderingData } from '../../../common/viewModel.js';
15
import { InlineDecoration } from '../../../common/viewModel/inlineDecorations.js';
16
import type { ViewContext } from '../../../common/viewModel/viewContext.js';
17
import type { ViewLineOptions } from '../../viewParts/viewLines/viewLineOptions.js';
18
import type { ITextureAtlasPageGlyph } from '../atlas/atlas.js';
19
import { createContentSegmenter, type IContentSegmenter } from '../contentSegmenter.js';
20
import { BindingId } from '../gpu.js';
21
import { GPULifecycle } from '../gpuDisposable.js';
22
import { quadVertices } from '../gpuUtils.js';
23
import { GlyphRasterizer } from '../raster/glyphRasterizer.js';
24
import { ViewGpuContext } from '../viewGpuContext.js';
25
import { BaseRenderStrategy } from './baseRenderStrategy.js';
26
import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js';
27
28
const enum Constants {
29
IndicesPerCell = 6,
30
CellBindBufferCapacityIncrement = 32,
31
CellBindBufferInitialCapacity = 63, // Will be rounded up to nearest increment
32
}
33
34
const enum CellBufferInfo {
35
FloatsPerEntry = 6,
36
BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4,
37
Offset_X = 0,
38
Offset_Y = 1,
39
Offset_Unused1 = 2,
40
Offset_Unused2 = 3,
41
GlyphIndex = 4,
42
TextureIndex = 5,
43
}
44
45
/**
46
* A render strategy that uploads the content of the entire viewport every frame.
47
*/
48
export class ViewportRenderStrategy extends BaseRenderStrategy {
49
/**
50
* The hard cap for line columns that can be rendered by the GPU renderer.
51
*/
52
static readonly maxSupportedColumns = 2000;
53
54
readonly type = 'viewport';
55
readonly wgsl: string = fullFileRenderStrategyWgsl;
56
57
private _cellBindBufferLineCapacity = Constants.CellBindBufferInitialCapacity;
58
private _cellBindBuffer!: GPUBuffer;
59
60
/**
61
* The cell value buffers, these hold the cells and their glyphs. It's double buffers such that
62
* the thread doesn't block when one is being uploaded to the GPU.
63
*/
64
private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer];
65
private _activeDoubleBufferIndex: 0 | 1 = 0;
66
67
private _visibleObjectCount: number = 0;
68
private _lastViewportLineCount: number = 0;
69
70
private _scrollOffsetBindBuffer: GPUBuffer;
71
private _scrollOffsetValueBuffer: Float32Array;
72
private _scrollInitialized: boolean = false;
73
74
get bindGroupEntries(): GPUBindGroupEntry[] {
75
return [
76
{ binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } },
77
{ binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } }
78
];
79
}
80
81
private readonly _onDidChangeBindGroupEntries = this._register(new Emitter<void>());
82
readonly onDidChangeBindGroupEntries = this._onDidChangeBindGroupEntries.event;
83
84
constructor(
85
context: ViewContext,
86
viewGpuContext: ViewGpuContext,
87
device: GPUDevice,
88
glyphRasterizer: { value: GlyphRasterizer },
89
) {
90
super(context, viewGpuContext, device, glyphRasterizer);
91
92
this._rebuildCellBuffer(this._cellBindBufferLineCapacity);
93
94
const scrollOffsetBufferSize = 2;
95
this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
96
label: 'Monaco scroll offset buffer',
97
size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT,
98
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
99
})).object;
100
this._scrollOffsetValueBuffer = new Float32Array(scrollOffsetBufferSize);
101
}
102
103
private _rebuildCellBuffer(lineCount: number) {
104
this._cellBindBuffer?.destroy();
105
106
// Increase in chunks so resizing a window by hand doesn't keep allocating and throwing away
107
const lineCountWithIncrement = (Math.floor(lineCount / Constants.CellBindBufferCapacityIncrement) + 1) * Constants.CellBindBufferCapacityIncrement;
108
109
const bufferSize = lineCountWithIncrement * ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT;
110
this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, {
111
label: 'Monaco full file cell buffer',
112
size: bufferSize,
113
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
114
})).object;
115
this._cellValueBuffers = [
116
new ArrayBuffer(bufferSize),
117
new ArrayBuffer(bufferSize),
118
];
119
this._cellBindBufferLineCapacity = lineCountWithIncrement;
120
this._lastViewportLineCount = 0;
121
122
this._onDidChangeBindGroupEntries.fire();
123
}
124
125
// #region Event handlers
126
127
// The primary job of these handlers is to:
128
// 1. Invalidate the up to date line cache, which will cause the line to be re-rendered when
129
// it's _within the viewport_.
130
// 2. Pass relevant events on to the render function so it can force certain line ranges to be
131
// re-rendered even if they're not in the viewport. For example when a view zone is added,
132
// there are lines that used to be visible but are no longer, so those ranges must be
133
// cleared and uploaded to the GPU.
134
135
public override onConfigurationChanged(e: ViewConfigurationChangedEvent): boolean {
136
return true;
137
}
138
139
public override onDecorationsChanged(e: ViewDecorationsChangedEvent): boolean {
140
return true;
141
}
142
143
public override onTokensChanged(e: ViewTokensChangedEvent): boolean {
144
return true;
145
}
146
147
public override onLinesDeleted(e: ViewLinesDeletedEvent): boolean {
148
return true;
149
}
150
151
public override onLinesInserted(e: ViewLinesInsertedEvent): boolean {
152
return true;
153
}
154
155
public override onLinesChanged(e: ViewLinesChangedEvent): boolean {
156
return true;
157
}
158
159
public override onScrollChanged(e?: ViewScrollChangedEvent): boolean {
160
if (this._store.isDisposed) {
161
return false;
162
}
163
const dpr = getActiveWindow().devicePixelRatio;
164
this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr;
165
this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr;
166
this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, this._scrollOffsetValueBuffer as Float32Array<ArrayBuffer>);
167
return true;
168
}
169
170
public override onThemeChanged(e: ViewThemeChangedEvent): boolean {
171
return true;
172
}
173
174
public override onLineMappingChanged(e: ViewLineMappingChangedEvent): boolean {
175
return true;
176
}
177
178
public override onZonesChanged(e: ViewZonesChangedEvent): boolean {
179
return true;
180
}
181
182
// #endregion
183
184
reset() {
185
for (const bufferIndex of [0, 1]) {
186
// Zero out buffer and upload to GPU to prevent stale rows from rendering
187
const buffer = new Float32Array(this._cellValueBuffers[bufferIndex]);
188
buffer.fill(0, 0, buffer.length);
189
this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength);
190
}
191
this._lastViewportLineCount = 0;
192
}
193
194
update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number {
195
// IMPORTANT: This is a hot function. Variables are pre-allocated and shared within the
196
// loop. This is done so we don't need to trust the JIT compiler to do this optimization to
197
// avoid potential additional blocking time in garbage collector which is a common cause of
198
// dropped frames.
199
200
let chars = '';
201
let segment: string | undefined;
202
let charWidth = 0;
203
let y = 0;
204
let x = 0;
205
let absoluteOffsetX = 0;
206
let absoluteOffsetY = 0;
207
let tabXOffset = 0;
208
let glyph: Readonly<ITextureAtlasPageGlyph>;
209
let cellIndex = 0;
210
211
let tokenStartIndex = 0;
212
let tokenEndIndex = 0;
213
let tokenMetadata = 0;
214
215
let decorationStyleSetBold: boolean | undefined;
216
let decorationStyleSetColor: number | undefined;
217
let decorationStyleSetOpacity: number | undefined;
218
let decorationStyleSetStrikethrough: boolean | undefined;
219
let decorationStyleSetStrikethroughThickness: number | undefined;
220
let decorationStyleSetStrikethroughColor: number | undefined;
221
222
let lineData: ViewLineRenderingData;
223
let decoration: InlineDecoration;
224
let fillStartIndex = 0;
225
let fillEndIndex = 0;
226
227
let tokens: IViewLineTokens;
228
229
const dpr = getActiveWindow().devicePixelRatio;
230
let contentSegmenter: IContentSegmenter;
231
232
if (!this._scrollInitialized) {
233
this.onScrollChanged();
234
this._scrollInitialized = true;
235
}
236
237
// Zero out cell buffer or rebuild if needed
238
if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) {
239
this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1);
240
}
241
const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);
242
cellBuffer.fill(0);
243
244
const lineIndexCount = ViewportRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;
245
246
for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) {
247
248
// Only attempt to render lines that the GPU renderer can handle
249
if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, y)) {
250
continue;
251
}
252
253
lineData = viewportData.getViewLineRenderingData(y);
254
tabXOffset = 0;
255
256
contentSegmenter = createContentSegmenter(lineData, viewLineOptions);
257
charWidth = viewLineOptions.spaceWidth * dpr;
258
absoluteOffsetX = (lineData.minColumn - 1) * charWidth;
259
260
tokens = lineData.tokens;
261
tokenStartIndex = lineData.minColumn - 1;
262
tokenEndIndex = 0;
263
for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) {
264
tokenEndIndex = tokens.getEndOffset(tokenIndex);
265
if (tokenEndIndex <= tokenStartIndex) {
266
// The faux indent part of the line should have no token type
267
continue;
268
}
269
270
tokenMetadata = tokens.getMetadata(tokenIndex);
271
272
for (x = tokenStartIndex; x < tokenEndIndex; x++) {
273
// Only render lines that do not exceed maximum columns
274
if (x > ViewportRenderStrategy.maxSupportedColumns) {
275
break;
276
}
277
segment = contentSegmenter.getSegmentAtIndex(x);
278
if (segment === undefined) {
279
continue;
280
}
281
chars = segment;
282
283
if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) {
284
charWidth = this.glyphRasterizer.getTextMetrics(chars).width;
285
}
286
287
decorationStyleSetColor = undefined;
288
decorationStyleSetBold = undefined;
289
decorationStyleSetOpacity = undefined;
290
decorationStyleSetStrikethrough = undefined;
291
decorationStyleSetStrikethroughThickness = undefined;
292
decorationStyleSetStrikethroughColor = undefined;
293
294
// Apply supported inline decoration styles to the cell metadata
295
for (decoration of lineData.inlineDecorations) {
296
// This is Range.strictContainsPosition except it works at the cell level,
297
// it's also inlined to avoid overhead.
298
if (
299
(y < decoration.range.startLineNumber || y > decoration.range.endLineNumber) ||
300
(y === decoration.range.startLineNumber && x < decoration.range.startColumn - 1) ||
301
(y === decoration.range.endLineNumber && x >= decoration.range.endColumn - 1)
302
) {
303
continue;
304
}
305
306
const rules = ViewGpuContext.decorationCssRuleExtractor.getStyleRules(this._viewGpuContext.canvas.domNode, decoration.inlineClassName);
307
for (const rule of rules) {
308
for (const r of rule.style) {
309
const value = rule.styleMap.get(r)?.toString() ?? '';
310
switch (r) {
311
case 'color': {
312
// TODO: This parsing and error handling should move into canRender so fallback
313
// to DOM works
314
const parsedColor = Color.Format.CSS.parse(value);
315
if (!parsedColor) {
316
throw new BugIndicatingError('Invalid color format ' + value);
317
}
318
decorationStyleSetColor = parsedColor.toNumber32Bit();
319
break;
320
}
321
case 'font-weight': {
322
const parsedValue = parseCssFontWeight(value);
323
if (parsedValue >= 400) {
324
decorationStyleSetBold = true;
325
// TODO: Set bold (https://github.com/microsoft/vscode/issues/237584)
326
} else {
327
decorationStyleSetBold = false;
328
// TODO: Set normal (https://github.com/microsoft/vscode/issues/237584)
329
}
330
break;
331
}
332
case 'opacity': {
333
const parsedValue = parseCssOpacity(value);
334
decorationStyleSetOpacity = parsedValue;
335
break;
336
}
337
case 'text-decoration':
338
case 'text-decoration-line': {
339
if (value === 'line-through') {
340
decorationStyleSetStrikethrough = true;
341
}
342
break;
343
}
344
case 'text-decoration-thickness': {
345
const match = value.match(/^(\d+(?:\.\d+)?)px$/);
346
if (match) {
347
decorationStyleSetStrikethroughThickness = parseFloat(match[1]);
348
}
349
break;
350
}
351
case 'text-decoration-color': {
352
let colorValue = value;
353
const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/);
354
if (varMatch) {
355
colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]);
356
}
357
const parsedColor = Color.Format.CSS.parse(colorValue);
358
if (parsedColor) {
359
decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit();
360
}
361
break;
362
}
363
case 'text-decoration-style': {
364
// These are validated in canRender and use default behavior
365
break;
366
}
367
default: throw new BugIndicatingError('Unexpected inline decoration style');
368
}
369
}
370
}
371
}
372
373
if (chars === ' ' || chars === '\t') {
374
// Zero out glyph to ensure it doesn't get rendered
375
cellIndex = ((y - 1) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;
376
cellBuffer.fill(0, cellIndex, cellIndex + CellBufferInfo.FloatsPerEntry);
377
// Adjust xOffset for tab stops
378
if (chars === '\t') {
379
// Find the pixel offset between the current position and the next tab stop
380
const offsetBefore = x + tabXOffset;
381
tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize);
382
absoluteOffsetX += charWidth * (tabXOffset - offsetBefore);
383
// Convert back to offset excluding x and the current character
384
tabXOffset -= x + 1;
385
} else {
386
absoluteOffsetX += charWidth;
387
}
388
continue;
389
}
390
391
const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor);
392
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);
393
394
absoluteOffsetY = Math.round(
395
// Top of layout box (includes line height)
396
viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] * dpr +
397
398
// Delta from top of layout box (includes line height) to top of the inline box (no line height)
399
Math.floor((viewportData.lineHeight * dpr - (glyph.fontBoundingBoxAscent + glyph.fontBoundingBoxDescent)) / 2) +
400
401
// Delta from top of inline box (no line height) to top of glyph origin. If the glyph was drawn
402
// with a top baseline for example, this ends up drawing the glyph correctly using the alphabetical
403
// baseline.
404
glyph.fontBoundingBoxAscent
405
);
406
407
cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;
408
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);
409
cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;
410
cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;
411
cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;
412
413
// Adjust the x pixel offset for the next character
414
absoluteOffsetX += charWidth;
415
}
416
417
tokenStartIndex = tokenEndIndex;
418
}
419
420
// Clear to end of line
421
fillStartIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + tokenEndIndex) * Constants.IndicesPerCell;
422
fillEndIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns) * Constants.IndicesPerCell;
423
cellBuffer.fill(0, fillStartIndex, fillEndIndex);
424
}
425
426
const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount;
427
const viewportLineCount = viewportData.endLineNumber - viewportData.startLineNumber + 1;
428
429
// This render strategy always uploads the whole viewport
430
this._device.queue.writeBuffer(
431
this._cellBindBuffer,
432
0,
433
cellBuffer.buffer,
434
0,
435
visibleObjectCount * Float32Array.BYTES_PER_ELEMENT
436
);
437
438
// Clear stale lines in GPU buffer if viewport shrunk
439
if (viewportLineCount < this._lastViewportLineCount) {
440
const staleLineCount = this._lastViewportLineCount - viewportLineCount;
441
const staleStartOffset = visibleObjectCount * Float32Array.BYTES_PER_ELEMENT;
442
const staleByteCount = staleLineCount * lineIndexCount * Float32Array.BYTES_PER_ELEMENT;
443
// Write zeros from the zeroed cellBuffer for the stale region
444
this._device.queue.writeBuffer(
445
this._cellBindBuffer,
446
staleStartOffset,
447
cellBuffer.buffer,
448
visibleObjectCount * Float32Array.BYTES_PER_ELEMENT,
449
staleByteCount
450
);
451
}
452
this._lastViewportLineCount = viewportLineCount;
453
454
this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;
455
456
this._visibleObjectCount = visibleObjectCount;
457
458
return visibleObjectCount;
459
}
460
461
draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {
462
if (this._visibleObjectCount <= 0) {
463
throw new BugIndicatingError('Attempt to draw 0 objects');
464
}
465
pass.draw(quadVertices.length / 2, this._visibleObjectCount);
466
}
467
}
468
469
function parseCssFontWeight(value: string) {
470
switch (value) {
471
case 'lighter':
472
case 'normal': return 400;
473
case 'bolder':
474
case 'bold': return 700;
475
}
476
return parseInt(value);
477
}
478
479
function parseCssOpacity(value: string): number {
480
if (value.endsWith('%')) {
481
return parseFloat(value.substring(0, value.length - 1)) / 100;
482
}
483
if (value.match(/^\d+(?:\.\d*)/)) {
484
return parseFloat(value);
485
}
486
return 1;
487
}
488
489