Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track buffer dirty changes and only upload dirty ranges #228843

Merged
merged 3 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions src/vs/editor/browser/gpu/bufferDirtyTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

export interface IBufferDirtyTrackerReader {
/**
* The index of the first dirty index.
*/
readonly dataOffset: number | undefined;
/**
* The index of the last dirty index (inclusive).
*/
readonly dirtySize: number | undefined;
/**
* Whether the buffer is dirty.
*/
readonly isDirty: boolean;
/**
* Clear the dirty state.
*/
clear(): void;
}

/**
* A simple tracker for dirty regions in a buffer.
*/
export class BufferDirtyTracker implements IBufferDirtyTrackerReader {

private _startIndex: number | undefined;
private _endIndex: number | undefined;

get dataOffset(): number | undefined {
return this._startIndex;
}

get dirtySize(): number | undefined {
if (this._startIndex === undefined || this._endIndex === undefined) {
return undefined;
}
return this._endIndex - this._startIndex + 1;
}

get isDirty(): boolean { return this._startIndex !== undefined; }

/**
* Flag the index(es) as modified. Returns the index flagged.
* @param index An index to flag.
* @param length An optional length to flag. Defaults to 1.
*/
flag(index: number, length: number = 1): number {
this._flag(index);
if (length > 1) {
this._flag(index + length - 1);
}
return index;
}

private _flag(index: number) {
if (this._startIndex === undefined || index < this._startIndex) {
this._startIndex = index;
}
if (this._endIndex === undefined || index > this._endIndex) {
this._endIndex = index;
}
}

clear() {
this._startIndex = undefined;
this._endIndex = undefined;
}
}
18 changes: 16 additions & 2 deletions src/vs/editor/browser/gpu/objectCollectionBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, dispose, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js';
import { LinkedList } from '../../../base/common/linkedList.js';
import { BufferDirtyTracker, type IBufferDirtyTrackerReader } from './bufferDirtyTracker.js';

export interface ObjectCollectionBufferPropertySpec {
name: string;
Expand Down Expand Up @@ -37,6 +38,11 @@ export interface IObjectCollectionBuffer<T extends ObjectCollectionBufferPropert
*/
readonly entryCount: number;

/**
* A tracker for dirty regions in the buffer.
*/
readonly dirtyTracker: IBufferDirtyTrackerReader;

/**
* Fires when the buffer is modified.
*/
Expand Down Expand Up @@ -81,6 +87,9 @@ class ObjectCollectionBuffer<T extends ObjectCollectionBufferPropertySpec[]> ext
return this._entries.size;
}

private _dirtyTracker = new BufferDirtyTracker();
get dirtyTracker(): IBufferDirtyTrackerReader { return this._dirtyTracker; }

private readonly _propertySpecsMap: Map<string, ObjectCollectionBufferPropertySpec & { offset: number }> = new Map();
private readonly _entrySize: number;
private readonly _entries: LinkedList<ObjectCollectionBufferEntry<T>> = new LinkedList();
Expand Down Expand Up @@ -112,7 +121,7 @@ class ObjectCollectionBuffer<T extends ObjectCollectionBufferPropertySpec[]> ext
throw new Error(`Cannot create more entries ObjectCollectionBuffer entries (capacity=${this.capacity})`);
}

const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._entries.size, data);
const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._dirtyTracker, this._entries.size, data);
const removeFromEntries = this._entries.push(value);
const listeners: IDisposable[] = [];
listeners.push(Event.forward(value.onDidChange, this._onDidChange));
Expand All @@ -129,6 +138,7 @@ class ObjectCollectionBuffer<T extends ObjectCollectionBufferPropertySpec[]> ext
entry.i--;
}
}
this._dirtyTracker.flag(deletedEntryIndex, (this._entries.size - deletedEntryIndex) * this._entrySize);
dispose(listeners);
}));
return value;
Expand All @@ -145,13 +155,15 @@ class ObjectCollectionBufferEntry<T extends ObjectCollectionBufferPropertySpec[]
constructor(
private _view: Float32Array,
private _propertySpecsMap: Map<string, ObjectCollectionBufferPropertySpec & { offset: number }>,
private _dirtyTracker: BufferDirtyTracker,
public i: number,
data: ObjectCollectionPropertyValues<T>,
) {
super();
for (const propertySpec of this._propertySpecsMap.values()) {
this._view[this.i * this._propertySpecsMap.size + propertySpec.offset] = data[propertySpec.name as keyof typeof data];
}
this._dirtyTracker.flag(this.i * this._propertySpecsMap.size, this._propertySpecsMap.size);
}

override dispose() {
Expand All @@ -160,7 +172,8 @@ class ObjectCollectionBufferEntry<T extends ObjectCollectionBufferPropertySpec[]
}

set(propertyName: T[number]['name'], value: number): void {
this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset] = value;
const i = this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset;
this._view[this._dirtyTracker.flag(i)] = value;
this._onDidChange.fire();
}

Expand All @@ -173,5 +186,6 @@ class ObjectCollectionBufferEntry<T extends ObjectCollectionBufferPropertySpec[]
throw new Error(`Data length ${data.length} does not match the number of properties in the collection (${this._propertySpecsMap.size})`);
}
this._view.set(data, this.i * this._propertySpecsMap.size);
this._dirtyTracker.flag(this.i * this._propertySpecsMap.size, this._propertySpecsMap.size);
}
}
7 changes: 5 additions & 2 deletions src/vs/editor/browser/gpu/rectangleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,11 @@ export class RectangleRenderer extends ViewEventHandler {
// --- end event handlers

private _update() {
// TODO: Only write dirty range
this._device.queue.writeBuffer(this._shapeBindBuffer, 0, this._shapeCollection.buffer);
const shapes = this._shapeCollection;
if (shapes.dirtyTracker.isDirty) {
this._device.queue.writeBuffer(this._shapeBindBuffer, 0, shapes.buffer, shapes.dirtyTracker.dataOffset, shapes.dirtyTracker.dirtySize! * shapes.view.BYTES_PER_ELEMENT);
shapes.dirtyTracker.clear();
}

// Update scroll offset
if (this._scrollChanged) {
Expand Down
46 changes: 46 additions & 0 deletions src/vs/editor/test/browser/view/gpu/bufferDirtyTracker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { strictEqual } from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { BufferDirtyTracker } from '../../../../browser/gpu/bufferDirtyTracker.js';

suite('BufferDirtyTracker', () => {
ensureNoDisposablesAreLeakedInTestSuite();

let bdt: BufferDirtyTracker;

function assertState(dataOffset: number | undefined, dirtySize: number | undefined) {
strictEqual(bdt.dataOffset, dataOffset);
strictEqual(bdt.dirtySize, dirtySize);
strictEqual(bdt.isDirty, dataOffset !== undefined);
}

setup(() => {
bdt = new BufferDirtyTracker();
});

test('flag(index)', () => {
strictEqual(bdt.flag(0), 0);
assertState(0, 1);
strictEqual(bdt.flag(31), 31);
assertState(0, 32);
bdt.clear();
assertState(undefined, undefined);
strictEqual(bdt.flag(10), 10);
assertState(10, 1);
strictEqual(bdt.flag(15), 15);
assertState(10, 6);
});

test('flag(index, length)', () => {
bdt.flag(0, 32);
assertState(0, 32);
bdt.clear();
assertState(undefined, undefined);
bdt.flag(10, 6);
assertState(10, 6);
});
});
Loading