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

Sub-pixel offset rendering #238699

Merged
merged 2 commits into from
Jan 24, 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
43 changes: 27 additions & 16 deletions src/vs/editor/browser/gpu/atlas/textureAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,16 @@ export class TextureAtlas extends Disposable {
this._onDidDeleteGlyphs.fire();
}

getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly<ITextureAtlasPageGlyph> {
getGlyph(rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number, x: number): Readonly<ITextureAtlasPageGlyph> {
// TODO: Encode font size and family into key
// Ignore metadata that doesn't affect the glyph
tokenMetadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK);

// Add x offset for sub-pixel rendering to the unused portion or tokenMetadata. This
// converts the decimal part of the x to a range from 0 to 9, where 0 = 0.0px x offset,
// 9 = 0.9px x offset
tokenMetadata |= Math.floor((x % 1) * 10);

// Warm up common glyphs
if (!this._warmedUpRasterizers.has(rasterizer.id)) {
this._warmUpAtlas(rasterizer);
Expand Down Expand Up @@ -167,27 +172,33 @@ export class TextureAtlas extends Disposable {
// Warm up using roughly the larger glyphs first to help optimize atlas allocation
// A-Z
for (let code = CharCode.A; code <= CharCode.Z; code++) {
taskQueue.enqueue(() => {
for (const fgColor of colorMap.keys()) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0);
}
});
for (const fgColor of colorMap.keys()) {
taskQueue.enqueue(() => {
for (let x = 0; x < 1; x += 0.1) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);
}
});
}
}
// a-z
for (let code = CharCode.a; code <= CharCode.z; code++) {
taskQueue.enqueue(() => {
for (const fgColor of colorMap.keys()) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0);
}
});
for (const fgColor of colorMap.keys()) {
taskQueue.enqueue(() => {
for (let x = 0; x < 1; x += 0.1) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);
}
});
}
}
// Remaining ascii
for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) {
taskQueue.enqueue(() => {
for (const fgColor of colorMap.keys()) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0);
}
});
for (const fgColor of colorMap.keys()) {
taskQueue.enqueue(() => {
for (let x = 0; x < 1; x += 0.1) {
this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK, 0, x);
}
});
}
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/vs/editor/browser/gpu/raster/glyphRasterizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {

this._ctx.save();

// The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this
// is used to improve the spacing between rendered characters.
const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10;

const bgId = TokenMetadata.getBackground(tokenMetadata);
const bg = colorMap[bgId];

Expand Down Expand Up @@ -157,7 +161,7 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer {
this._ctx.globalAlpha = decorationStyleSet.opacity;
}

this._ctx.fillText(chars, originX, originY);
this._ctx.fillText(chars, originX + xSubPixelXOffset, originY);
this._ctx.restore();

const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,6 @@ export class FullFileRenderStrategy extends BaseRenderStrategy {
this._scrollInitialized = true;
}

let localContentWidth = 0;

// Update cell data
const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]);
const lineIndexCount = FullFileRenderStrategy.maxSupportedColumns * Constants.IndicesPerCell;
Expand Down Expand Up @@ -445,7 +443,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy {
}

const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity);
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId);
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);

absoluteOffsetY = Math.round(
// Top of layout box (includes line height)
Expand All @@ -461,14 +459,13 @@ export class FullFileRenderStrategy extends BaseRenderStrategy {
);

cellIndex = ((y - 1) * FullFileRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX);
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);
cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;
cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;
cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;

// Adjust the x pixel offset for the next character
absoluteOffsetX += charWidth;
localContentWidth = Math.max(localContentWidth, absoluteOffsetX);
}

tokenStartIndex = tokenEndIndex;
Expand Down Expand Up @@ -504,7 +501,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy {

this._visibleObjectCount = visibleObjectCount;

return { localContentWidth };
return {
localContentWidth: absoluteOffsetX
};
}

draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,6 @@ export class ViewportRenderStrategy extends BaseRenderStrategy {
this._scrollInitialized = true;
}

let localContentWidth = 0;

// Zero out cell buffer or rebuild if needed
if (this._cellBindBufferLineCapacity < viewportData.endLineNumber - viewportData.startLineNumber + 1) {
this._rebuildCellBuffer(viewportData.endLineNumber - viewportData.startLineNumber + 1);
Expand Down Expand Up @@ -348,7 +346,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy {
}

const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity);
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId);
glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX);

absoluteOffsetY = Math.round(
// Top of layout box (includes line height)
Expand All @@ -364,14 +362,13 @@ export class ViewportRenderStrategy extends BaseRenderStrategy {
);

cellIndex = ((y - viewportData.startLineNumber) * ViewportRenderStrategy.maxSupportedColumns + x) * Constants.IndicesPerCell;
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.round(absoluteOffsetX);
cellBuffer[cellIndex + CellBufferInfo.Offset_X] = Math.floor(absoluteOffsetX);
cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = absoluteOffsetY;
cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex;
cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex;

// Adjust the x pixel offset for the next character
absoluteOffsetX += charWidth;
localContentWidth = Math.max(localContentWidth, absoluteOffsetX);
}

tokenStartIndex = tokenEndIndex;
Expand All @@ -397,7 +394,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy {
this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1;

this._visibleObjectCount = visibleObjectCount;
return { localContentWidth };
return {
localContentWidth: absoluteOffsetX
};
}

draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ export class ViewLinesGpu extends ViewPart implements IViewLines {
// Track the largest local content width so far in this session and use it as the scroll
// width. This is how the DOM renderer works as well, so you may not be able to scroll to
// the right in a file with long lines until you scroll down.
this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth);
this._maxLocalContentWidthSoFar = Math.max(this._maxLocalContentWidthSoFar, localContentWidth / this._viewGpuContext.devicePixelRatio.get());
this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLocalContentWidthSoFar);
this._viewGpuContext.scrollWidthElement.setWidth(this._context.viewLayout.getScrollWidth());

Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/contrib/gpu/browser/gpuActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class DebugEditorGpuRendererAction extends EditorAction {
}
const tokenMetadata = 0;
const charMetadata = 0;
const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata);
const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, tokenMetadata, charMetadata, 0);
if (!rasterizedGlyph) {
return;
}
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/test/browser/gpu/atlas/textureAtlas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ const blackInt = 0x000000FF;
const nullCharMetadata = 0x0;

let lastUniqueGlyph: string | undefined;
function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number] {
function getUniqueGlyphId(): [chars: string, tokenMetadata: number, charMetadata: number, x: number] {
if (!lastUniqueGlyph) {
lastUniqueGlyph = 'a';
} else {
lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1);
}
return [lastUniqueGlyph, blackInt, nullCharMetadata];
return [lastUniqueGlyph, blackInt, nullCharMetadata, 0];
}

class TestGlyphRasterizer implements IGlyphRasterizer {
Expand Down
Loading