Skip to content

Commit

Permalink
start of atlas garbage collection, with tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mikekucera committed Aug 14, 2024
1 parent ee4545f commit 66f5e4c
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 46 deletions.
9 changes: 7 additions & 2 deletions debug/webgl/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,13 @@ <h2>Network</h2>
<option value="gray">👽 Gray</option>
</select>
<br><br>
<button id="fit-button">Fit to view</button> <br />

<button id="fit-button">Fit to view</button>
<br><br>
<button id="select-button">Select all nodes</button>
<br><br>
<button id="delete-button">Delete selected</button>
<br><br>
<button id="gc-button">Garbage collect (beta)</button>
<br><br>
<h2>WebGL Settings</h2>
<div>
Expand Down
13 changes: 13 additions & 0 deletions debug/webgl/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,17 @@ const paramDefs = {

$("#fit-button").addEventListener('click', () => cy.fit());
$("#reset-button").addEventListener('click', () => reloadPage(true));

$("#delete-button").addEventListener('click', () => {
cy.remove(':selected');
});

$("#select-button").addEventListener('click', () => {
cy.nodes().select();
});

$("#gc-button").addEventListener('click', () => {
cy.gc();
});

})();
4 changes: 4 additions & 0 deletions src/core/viewport.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ let corefn = ({
return this; // chaining
},

gc: function() {
this.notify('gc');
},

fit: function( elements, padding ){
let viewportState = this.getFitViewport( elements, padding );

Expand Down
4 changes: 4 additions & 0 deletions src/extensions/renderer/base/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ BRp.notify = function( eventName, eles ) {
r.redrawHint( 'select', true );
}

if( eventName === 'gc' ){
r.redrawHint( 'gc', true );
}

if( eventName === 'load' || eventName === 'resize' || eventName === 'mount' ){
r.invalidateContainerClientCoordsCache();
r.matchCanvasSize( r.container );
Expand Down
5 changes: 4 additions & 1 deletion src/extensions/renderer/canvas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ function CanvasRenderer( options ){

CRp.redrawHint = function( group, bool ){
var r = this;

switch( group ){
case 'eles':
r.data.canvasNeedsRedraw[ CRp.NODE ] = bool;
Expand All @@ -306,6 +306,9 @@ CRp.redrawHint = function( group, bool ){
case 'select':
r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool;
break;
case 'gc':
r.data.gc = true;
break;
}
};

Expand Down
204 changes: 175 additions & 29 deletions src/extensions/renderer/canvas/webgl/atlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class Atlas {

this.atlasSize = opts.webglTexSize;
this.rows = opts.webglTexRows;

this.texHeight = Math.floor(this.atlasSize / this.rows);
this.maxTexWidth = this.atlasSize;

Expand All @@ -19,29 +20,30 @@ export class Atlas {

// a "location" is an object with a row and x fields
this.freePointer = { x: 0, row: 0 };

// map from the style key to the row/x where the texture starts
// if the texture wraps then there's a second location
this.keyToLocation = new Map(); // styleKey -> [ location, location ]
this.invalidKeys = new Set();

if(r) {
this.canvas = util.createTextureCanvas(r, this.atlasSize);
this.scratch = util.createTextureCanvas(r, this.atlasSize, this.texHeight);
}
this.canvas = opts.createTextureCanvas(r, this.atlasSize, this.atlasSize);
this.scratch = opts.createTextureCanvas(r, this.atlasSize, this.texHeight);
}

getKeys() {
return new Set(this.keyToLocation.keys());
}

getScale(bb) {
getScale({ w, h }) {
const { texHeight, maxTexWidth } = this;
// try to fit to the height of a row
let scale = texHeight / bb.h; // TODO what about pixelRatio?
let texW = bb.w * scale;
let texH = bb.h * scale;
let scale = texHeight / h; // TODO what about pixelRatio?
let texW = w * scale;
let texH = h * scale;
// if the scaled width is too wide then scale to fit max width instead
if(texW > maxTexWidth) {
scale = maxTexWidth / bb.w;
texW = bb.w * scale;
texH = bb.h * scale;
scale = maxTexWidth / w;
texW = w * scale;
texH = h * scale;
}
return { scale, texW, texH };
}
Expand Down Expand Up @@ -99,10 +101,8 @@ export class Atlas {
const { scratch, canvas } = this;

// Draw to the scratch canvas
if(scratch) {
scratch.clear();
drawAt({ x:0, row:0 }, scratch);
}
scratch.clear();
drawAt({ x:0, row:0 }, scratch);

const firstTexW = atlasSize - this.freePointer.x;
const secondTexW = texW - firstTexW;
Expand All @@ -113,9 +113,10 @@ export class Atlas {
const dy = this.freePointer.row * texHeight;
const w = firstTexW;

if(canvas) {
canvas.context.drawImage(scratch, 0, 0, w, h, dx, dy, w, h);
}
canvas.context.drawImage(scratch,
0, 0, w, h,
dx, dy, w, h
);

locations[0] = {
x: dx,
Expand All @@ -130,7 +131,10 @@ export class Atlas {
const w = secondTexW;

if(canvas) {
canvas.context.drawImage(scratch, sx, 0, w, h, 0, dy, w, h);
canvas.context.drawImage(scratch,
sx, 0, w, h,
0, dy, w, h
);
}

locations[1] = {
Expand Down Expand Up @@ -167,7 +171,7 @@ export class Atlas {
return true;
}

bufferIfNeeded(gl) {
bufferIfNeeded(gl, util) {
if(!this.buffered) {
this.texture = util.bufferTexture(gl, this.canvas);
this.buffered = true;
Expand All @@ -183,38 +187,171 @@ export class AtlasControl {
this.r = r;
this.opts = opts;

this.keyToIds = new Map();
this.idToKey = new Map();

this.atlases = [];
this.styleKeyToAtlas = new Map();
this.invalidKeys = new Set();
}

createAtlas() {
getKeys() {
return new Set(this.styleKeyToAtlas.keys());
}

getIdsFor(key) {
let ids = this.keyToIds.get(key);
if(!ids) {
ids = new Set();
this.keyToIds.set(key, ids);
}
return ids;
}

_createAtlas() {
const { r, opts } = this;
return new Atlas(r, opts);
}

markInvalid(key) {
if(this.styleKeyToAtlas.has(key)) {
this.invalidKeys.add(key);
_getScratchCanvas() {
if(!this.scratch) {
const { r, opts } = this;
const atlasSize = opts.webglTexSize;
const texHeight = Math.floor(atlasSize / opts.webglTexRows);
this.scratch = this.opts.createTextureCanvas(r, atlasSize, texHeight);
}
return this.scratch;
}

getAtlas(key, bb, doDrawing) {
draw(key, id, bb, doDrawing) {
let atlas = this.styleKeyToAtlas.get(key);
if(!atlas) {
// this is an overly simplistic way of finding an atlas, needs to be rewritten
atlas = this.atlases[this.atlases.length - 1];
if(!atlas || !atlas.canFit(bb)) {
atlas = this.createAtlas();
atlas = this._createAtlas();
this.atlases.push(atlas);
}
this.styleKeyToAtlas.set(key, atlas);

atlas.draw(key, bb, doDrawing);

this.styleKeyToAtlas.set(key, atlas);
this.getIdsFor(key).add(id);
this.idToKey.set(id, key);
}
return atlas;
}

getAtlas(key) {
return this.styleKeyToAtlas.get(key);
}

invalidate(newKey, id) {
if(!this.idToKey.has(id))
return;

const oldKey = this.idToKey.get(id);
if(oldKey != newKey) {
this.idToKey.delete(id);
this.getIdsFor(oldKey).delete(id);
}
}

_getKeysToCollect() {
const markedKeys = new Set();
for(const key of this.styleKeyToAtlas.keys()) {
if(this.getIdsFor(key).size == 0) {
markedKeys.add(key);
}
}
return markedKeys;
}


gc() {
const markedKeys = this._getKeysToCollect();
if(markedKeys.size === 0) {
console.log("nothing to garbage collect");
return;
}

const newAtlases = [];
const newStyleKeyToAtlas = new Map();

let newAtlas = null;

for(const atlas of this.atlases) {
const keys = atlas.getKeys();

const keysToCollect = intersection(markedKeys, keys);

if(keysToCollect.size === 0) {
newAtlases.push(atlas);
keys.forEach(k => newStyleKeyToAtlas.set(k, atlas));
continue;
}

if(!newAtlas) {
newAtlas = this._createAtlas();
newAtlases.push(newAtlas);
}

for(const key of keys) {
if(!keysToCollect.has(key)) {
const [ s1, s2 ] = atlas.getTexOffsets(key);
if(!newAtlas.canFit({ w: s1.w + s2.w, h: s1.h })) {
newAtlas = this._createAtlas();
newAtlases.push(newAtlas);
}
this._copyTextureToNewAtlas(key, atlas, newAtlas);
newStyleKeyToAtlas.set(key, newAtlas);
}
}

}

this.atlases = newAtlases;
this.styleKeyToAtlas = newStyleKeyToAtlas;
// TODO, I might not clean up every key
this.markedKeys = new Set();
}


_copyTextureToNewAtlas(key, oldAtlas, newAtlas) {
const [ s1, s2 ] = oldAtlas.getTexOffsets(key);

if(s2.w === 0) { // the texture does not wrap, draw directly to new atlas
newAtlas.draw(key, s1, context => {
context.drawImage(oldAtlas.canvas,
s1.x, s1.y, s1.w, s1.h,
0, 0, s1.w, s1.h
);
});
} else {
// the texture wraps, first draw both parts to a scratch canvas
const scratch = this._getScratchCanvas();
scratch.clear();
scratch.context.drawImage(oldAtlas.canvas,
s1.x, s1.y, s1.w, s1.h,
0, 0, s1.w, s1.h
);
scratch.context.drawImage(oldAtlas.canvas,
s2.x, s2.y, s2.w, s2.h,
s1.w, 0, s2.w, s2.h
);

// now draw the scratch to the new atlas
const w = s1.w + s2.w;
const h = s1.h;
newAtlas.draw(key, { w, h }, context => {
context.drawImage(scratch,
0, 0, w, h,
0, 0, w, h // the destination context has already been translated to the correct position
);
});
}
}


getCounts() {
return {
keyCount: this.styleKeyToAtlas.size,
Expand All @@ -223,3 +360,12 @@ export class AtlasControl {
}

}


function intersection(set1, set2) {
// TODO why no Set.intersection in node 16???
if(set1.intersection)
return set1.intersection(set2);
else
return new Set([...set1].filter(x => set2.has(x)));
}
Loading

0 comments on commit 66f5e4c

Please sign in to comment.