Skip to content

Fix corrupted textures when rendering too many WebGL characters #7792

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

Merged
merged 1 commit into from
May 6, 2025
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
2 changes: 0 additions & 2 deletions src/type/p5.Font.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const invalidFontError = 'Sorry, only TTF, OTF and WOFF files are supported.'; /
const fontFaceVariations = ['weight', 'stretch', 'style'];


let nextId = 0;
export class Font {
constructor(p, fontFace, name, path, data) {
if (!(fontFace instanceof FontFace)) {
Expand All @@ -62,7 +61,6 @@ export class Font {
this.path = path;
this.data = data;
this.face = fontFace;
this.id = nextId++;
}

/**
Expand Down
58 changes: 28 additions & 30 deletions src/webgl/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,9 @@ import { Font, arrayCommandsToObjects } from "../type/p5.Font";
function text(p5, fn) {
RendererGL.prototype.maxCachedGlyphs = function() {
// TODO: use more than vibes to find a good value for this
return 200
return 200;
};

RendererGL.prototype.freeGlyphInfo = function(gi) {
const datas = [
gi.strokeImageInfo.imageData,
gi.rowInfo.cellImageInfo.imageData,
gi.rowInfo.dimImageInfo.imageData,
gi.colInfo.cellImageInfo.imageData,
gi.colInfo.dimImageInfo.imageData,
];
for (const data of datas) {
const tex = this.textures.get(data);
if (tex) {
tex.remove();
this.textures.delete(data);
}
}
}

Font.prototype._getFontInfo = function(axs) {
// For WebGL, a cache of font data to use on the GPU.
this._fontInfos = this._fontInfos || {};
Expand Down Expand Up @@ -792,7 +775,7 @@ function text(p5, fn) {
sh.setUniform("uMaterialColor", curFillColor);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

this.fontCache = this.fontCache || new Map();
this.glyphDataCache = this.glyphDataCache || new Set();

try {
// fetch the glyphs in the line of text
Expand All @@ -801,20 +784,35 @@ function text(p5, fn) {
for (const glyph of glyphs) {
const gi = fontInfo.getGlyphInfo(glyph);
if (gi.uGlyphRect) {
const rowInfo = gi.rowInfo;
const colInfo = gi.colInfo;

const cacheKey = JSON.stringify({ font: font.id, axs, glyph: glyph.shape.g });
// Bump this font to the end of the cache list by deleting and re-adding it
this.fontCache.delete(cacheKey);
this.fontCache.set(cacheKey, gi);
if (this.fontCache.size > this.maxCachedGlyphs()) {
const keyToRemove = this.fontCache.keys().next().value;
const val = this.fontCache.get(keyToRemove);
this.fontCache.delete(keyToRemove);
this.freeGlyphInfo(val);
// Bump the resources for this glyph to the end of the cache list by deleting and re-adding
const glyphResources = [
gi.strokeImageInfo.imageData,
rowInfo.cellImageInfo.imageData,
rowInfo.dimImageInfo.imageData,
colInfo.cellImageInfo.imageData,
colInfo.dimImageInfo.imageData
];
for (const resource of glyphResources) {
this.glyphDataCache.delete(resource);
this.glyphDataCache.add(resource);
}

// If we have too many glyph textures, remove the least recently used
// ones from GPU memory. The data still exists on the CPU and will be
// re-uploaded if it gets actively used again.
while (this.glyphDataCache.size > this.maxCachedGlyphs()) {
const data = this.glyphDataCache.values().next().value;
this.glyphDataCache.delete(data);
const tex = this.textures.get(data);
if (tex) {
tex.remove();
this.textures.delete(data);
}
}

const rowInfo = gi.rowInfo;
const colInfo = gi.colInfo;
sh.setUniform("uSamplerStrokes", gi.strokeImageInfo.imageData);
sh.setUniform("uSamplerRowStrokes", rowInfo.cellImageInfo.imageData);
sh.setUniform("uSamplerRows", rowInfo.dimImageInfo.imageData);
Expand Down
29 changes: 29 additions & 0 deletions test/unit/visual/cases/webgl.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { vi, beforeEach, afterEach } from 'vitest';
import { visualSuite, visualTest } from '../visualTest';
import { RendererGL } from '../../../../src/webgl/p5.RendererGL';

visualSuite('WebGL', function() {
visualSuite('Camera', function() {
Expand Down Expand Up @@ -631,4 +633,31 @@ visualSuite('WebGL', function() {
screenshot();
});
});

visualSuite('font data', () => {
afterEach(() => {
vi.restoreAllMocks();
});

visualTest('glyph resource allocation does not corrupt textures', async (p5, screenshot) => {
p5.createCanvas(100, 100, p5.WEBGL);
vi.spyOn(p5._renderer, 'maxCachedGlyphs').mockReturnValue(6);

const font = await p5.loadFont(
'/unit/assets/Inconsolata-Bold.ttf'
);

p5.textFont(font);
p5.clear();
p5.textSize(10);
p5.textAlign(p5.LEFT, p5.TOP);
for (let i = 0; i < 100; i++) {
const x = -p5.width/2 + (i % 10) * 10;
const y = -p5.height/2 + p5.floor(i / 10) * 10;
p5.text(String.fromCharCode(33 + i), x, y);
}

screenshot();
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"numScreenshots": 1
}