Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/browser/gpu/viewGpuContext.ts
5240 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 * as nls from '../../../nls.js';
7
import { addDisposableListener, getActiveWindow } from '../../../base/browser/dom.js';
8
import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js';
9
import { Color } from '../../../base/common/color.js';
10
import { BugIndicatingError } from '../../../base/common/errors.js';
11
import { Disposable } from '../../../base/common/lifecycle.js';
12
import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js';
13
import type { ViewLineOptions } from '../viewParts/viewLines/viewLineOptions.js';
14
import { observableValue, runOnChange, type IObservable } from '../../../base/common/observable.js';
15
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
16
import { TextureAtlas } from './atlas/textureAtlas.js';
17
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
18
import { INotificationService, IPromptChoice, Severity } from '../../../platform/notification/common/notification.js';
19
import { IThemeService } from '../../../platform/theme/common/themeService.js';
20
import { GPULifecycle } from './gpuDisposable.js';
21
import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js';
22
import { RectangleRenderer } from './rectangleRenderer.js';
23
import type { ViewContext } from '../../common/viewModel/viewContext.js';
24
import { DecorationCssRuleExtractor } from './css/decorationCssRuleExtractor.js';
25
import { Event } from '../../../base/common/event.js';
26
import { EditorOption, type IEditorOptions } from '../../common/config/editorOptions.js';
27
import { DecorationStyleCache } from './css/decorationStyleCache.js';
28
import { InlineDecorationType } from '../../common/viewModel/inlineDecorations.js';
29
30
export class ViewGpuContext extends Disposable {
31
/**
32
* The hard cap for line columns rendered by the GPU renderer.
33
*/
34
readonly maxGpuCols = 2000;
35
36
readonly canvas: FastDomNode<HTMLCanvasElement>;
37
readonly ctx: GPUCanvasContext;
38
39
static device: Promise<GPUDevice>;
40
static deviceSync: GPUDevice | undefined;
41
42
readonly rectangleRenderer: RectangleRenderer;
43
44
private static readonly _decorationCssRuleExtractor = new DecorationCssRuleExtractor();
45
static get decorationCssRuleExtractor(): DecorationCssRuleExtractor {
46
return ViewGpuContext._decorationCssRuleExtractor;
47
}
48
49
private static readonly _decorationStyleCache = new DecorationStyleCache();
50
static get decorationStyleCache(): DecorationStyleCache {
51
return ViewGpuContext._decorationStyleCache;
52
}
53
54
private static _atlas: TextureAtlas | undefined;
55
56
/**
57
* The shared texture atlas to use across all views.
58
*
59
* @throws if called before the GPU device is resolved
60
*/
61
static get atlas(): TextureAtlas {
62
if (!ViewGpuContext._atlas) {
63
throw new BugIndicatingError('Cannot call ViewGpuContext.textureAtlas before device is resolved');
64
}
65
return ViewGpuContext._atlas;
66
}
67
/**
68
* The shared texture atlas to use across all views. This is a convenience alias for
69
* {@link ViewGpuContext.atlas}.
70
*
71
* @throws if called before the GPU device is resolved
72
*/
73
get atlas(): TextureAtlas {
74
return ViewGpuContext.atlas;
75
}
76
77
readonly canvasDevicePixelDimensions: IObservable<{ width: number; height: number }>;
78
readonly devicePixelRatio: IObservable<number>;
79
readonly contentLeft: IObservable<number>;
80
81
constructor(
82
context: ViewContext,
83
@IInstantiationService private readonly _instantiationService: IInstantiationService,
84
@INotificationService private readonly _notificationService: INotificationService,
85
@IConfigurationService private readonly configurationService: IConfigurationService,
86
@IThemeService private readonly _themeService: IThemeService,
87
) {
88
super();
89
90
this.canvas = createFastDomNode(document.createElement('canvas'));
91
this.canvas.setClassName('editorCanvas');
92
93
// Adjust the canvas size to avoid drawing under the scroll bar
94
this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => {
95
if (!e || e.affectsConfiguration('editor.scrollbar.verticalScrollbarSize')) {
96
const verticalScrollbarSize = configurationService.getValue<IEditorOptions>('editor').scrollbar?.verticalScrollbarSize ?? 14;
97
this.canvas.domNode.style.boxSizing = 'border-box';
98
this.canvas.domNode.style.paddingRight = `${verticalScrollbarSize}px`;
99
}
100
}));
101
102
this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu'));
103
104
// Request the GPU device, we only want to do this a single time per window as it's async
105
// and can delay the initial render.
106
if (!ViewGpuContext.device) {
107
ViewGpuContext.device = GPULifecycle.requestDevice((message) => {
108
const choices: IPromptChoice[] = [{
109
label: nls.localize('editor.dom.render', "Use DOM-based rendering"),
110
run: () => this.configurationService.updateValue('editor.experimentalGpuAcceleration', 'off'),
111
}];
112
this._notificationService.prompt(Severity.Warning, message, choices);
113
}).then(ref => {
114
ViewGpuContext.deviceSync = ref.object;
115
if (!ViewGpuContext._atlas) {
116
ViewGpuContext._atlas = this._instantiationService.createInstance(TextureAtlas, ref.object.limits.maxTextureDimension2D, undefined, ViewGpuContext.decorationStyleCache);
117
}
118
return ref.object;
119
});
120
}
121
122
const dprObs = observableValue(this, getActiveWindow().devicePixelRatio);
123
this._register(addDisposableListener(getActiveWindow(), 'resize', () => {
124
dprObs.set(getActiveWindow().devicePixelRatio, undefined);
125
}));
126
this.devicePixelRatio = dprObs;
127
this._register(runOnChange(this.devicePixelRatio, () => ViewGpuContext.atlas?.clear()));
128
129
// Clear decoration CSS caches when theme changes as CSS variables may have different values
130
this._register(this._themeService.onDidColorThemeChange(() => {
131
ViewGpuContext.decorationCssRuleExtractor.clear();
132
ViewGpuContext.atlas?.clear();
133
}));
134
135
const canvasDevicePixelDimensions = observableValue(this, { width: this.canvas.domNode.width, height: this.canvas.domNode.height });
136
this._register(observeDevicePixelDimensions(
137
this.canvas.domNode,
138
getActiveWindow(),
139
(width, height) => {
140
this.canvas.domNode.width = width;
141
this.canvas.domNode.height = height;
142
canvasDevicePixelDimensions.set({ width, height }, undefined);
143
}
144
));
145
this.canvasDevicePixelDimensions = canvasDevicePixelDimensions;
146
147
const contentLeft = observableValue(this, 0);
148
this._register(this.configurationService.onDidChangeConfiguration(e => {
149
contentLeft.set(context.configuration.options.get(EditorOption.layoutInfo).contentLeft, undefined);
150
}));
151
this.contentLeft = contentLeft;
152
153
this.rectangleRenderer = this._register(this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device));
154
}
155
156
/**
157
* This method determines which lines can be and are allowed to be rendered using the GPU
158
* renderer. Eventually this should trend all lines, except maybe exceptional cases like
159
* decorations that use class names.
160
*/
161
public canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean {
162
const data = viewportData.getViewLineRenderingData(lineNumber);
163
164
// Check if the line has simple attributes that aren't supported
165
if (
166
data.containsRTL ||
167
data.maxColumn > this.maxGpuCols
168
) {
169
return false;
170
}
171
172
// Check if all inline decorations are supported
173
if (data.inlineDecorations.length > 0) {
174
let supported = true;
175
for (const decoration of data.inlineDecorations) {
176
if (decoration.type !== InlineDecorationType.Regular) {
177
supported = false;
178
break;
179
}
180
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);
181
supported &&= styleRules.every(rule => {
182
// Pseudo classes aren't supported currently
183
if (rule.selectorText.includes(':')) {
184
return false;
185
}
186
for (const r of rule.style) {
187
if (!supportsCssRule(r, rule.style)) {
188
return false;
189
}
190
}
191
return true;
192
});
193
if (!supported) {
194
break;
195
}
196
}
197
return supported;
198
}
199
200
return true;
201
}
202
203
/**
204
* Like {@link canRender} but returns detailed information about why the line cannot be rendered.
205
*/
206
public canRenderDetailed(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): string[] {
207
const data = viewportData.getViewLineRenderingData(lineNumber);
208
const reasons: string[] = [];
209
if (data.containsRTL) {
210
reasons.push('containsRTL');
211
}
212
if (data.maxColumn > this.maxGpuCols) {
213
reasons.push('maxColumn > maxGpuCols');
214
}
215
if (data.inlineDecorations.length > 0) {
216
let supported = true;
217
const problemTypes: InlineDecorationType[] = [];
218
const problemSelectors: string[] = [];
219
const problemRules: string[] = [];
220
for (const decoration of data.inlineDecorations) {
221
if (decoration.type !== InlineDecorationType.Regular) {
222
problemTypes.push(decoration.type);
223
supported = false;
224
continue;
225
}
226
const styleRules = ViewGpuContext._decorationCssRuleExtractor.getStyleRules(this.canvas.domNode, decoration.inlineClassName);
227
supported &&= styleRules.every(rule => {
228
// Pseudo classes aren't supported currently
229
if (rule.selectorText.includes(':')) {
230
problemSelectors.push(rule.selectorText);
231
return false;
232
}
233
for (const r of rule.style) {
234
if (!supportsCssRule(r, rule.style)) {
235
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
236
problemRules.push(`${r}: ${rule.style[r as any]}`);
237
return false;
238
}
239
}
240
return true;
241
});
242
if (!supported) {
243
continue;
244
}
245
}
246
if (problemTypes.length > 0) {
247
reasons.push(`inlineDecorations with unsupported types (${problemTypes.map(e => `\`${e}\``).join(', ')})`);
248
}
249
if (problemRules.length > 0) {
250
reasons.push(`inlineDecorations with unsupported CSS rules (${problemRules.map(e => `\`${e}\``).join(', ')})`);
251
}
252
if (problemSelectors.length > 0) {
253
reasons.push(`inlineDecorations with unsupported CSS selectors (${problemSelectors.map(e => `\`${e}\``).join(', ')})`);
254
}
255
}
256
return reasons;
257
}
258
}
259
260
/**
261
* A list of supported decoration CSS rules that can be used in the GPU renderer.
262
*/
263
const gpuSupportedDecorationCssRules = [
264
'color',
265
'font-weight',
266
'opacity',
267
'text-decoration',
268
'text-decoration-color',
269
'text-decoration-line',
270
'text-decoration-style',
271
'text-decoration-thickness',
272
];
273
274
function supportsCssRule(rule: string, style: CSSStyleDeclaration) {
275
if (!gpuSupportedDecorationCssRules.includes(rule)) {
276
return false;
277
}
278
// Check for values that aren't supported
279
switch (rule) {
280
case 'text-decoration':
281
case 'text-decoration-line': {
282
const value = style.getPropertyValue(rule);
283
// Only line-through is supported currently
284
return value === 'line-through';
285
}
286
case 'text-decoration-color': {
287
const value = style.getPropertyValue(rule);
288
// Support var(--something, initial/inherit) which falls back to currentcolor
289
if (/^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value)) {
290
return true;
291
}
292
// Support parsed color values
293
return Color.Format.CSS.parse(value) !== null;
294
}
295
case 'text-decoration-style': {
296
const value = style.getPropertyValue(rule);
297
// Only 'initial' (solid) is supported
298
return value === 'initial';
299
}
300
case 'text-decoration-thickness': {
301
const value = style.getPropertyValue(rule);
302
// Only pixel values and 'initial' are supported
303
return value === 'initial' || /^\d+(\.\d+)?px$/.test(value);
304
}
305
default: return true;
306
}
307
}
308
309