Skip to content

Commit

Permalink
Merge pull request microsoft#238699 from microsoft/tyriar/225428_subp…
Browse files Browse the repository at this point in the history
…ixel_offset

Sub-pixel offset rendering
  • Loading branch information
Tyriar authored Jan 24, 2025
2 parents baf5cf7 + a9e5a0e commit 48ae5ad
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 33 deletions.
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

0 comments on commit 48ae5ad

Please sign in to comment.