This repository has been archived by the owner on Apr 1, 2020. It is now read-only.
Add experimental WebGL Renderer #2120
Merged
bryphe
merged 9 commits into
onivim:master
from
manu-unter:cryza/performance/webgl-renderer
Apr 25, 2018
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f068b30
First version of the WebGL renderer with grayscale text antialias
mhornung f576c51
Add CachedColorNormalizer and first attempt at background rendering
mhornung 4bebe04
Merge commit 'b96c4cfe2ff7bbe5c97a11fd4b1e869dbe5f5c5f' into cryza/pe…
mhornung fd03b20
Get background renderer to work :)
mhornung 3b4888a
Merge commit '01b7ee03aa4c152a01de525df5becb6f302de661' into cryza/pe…
mhornung 74261b0
Refactor WebGL code
mhornung d28e5b6
Incorporate line padding into glyph positioning
mhornung b759e28
Add configuration property for switching between canvas and WebGL ren…
mhornung 04d5eaa
Improve warning and tweak atlas padding
mhornung File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import * as normalizeColor from "color-normalize" | ||
import { IColorNormalizer } from "./IColorNormalizer" | ||
|
||
export class CachedColorNormalizer implements IColorNormalizer { | ||
private _cache = new Map<string, Float32Array>() | ||
|
||
public normalizeColor(cssColor: string): Float32Array { | ||
const cachedRgba = this._cache.get(cssColor) | ||
|
||
if (cachedRgba) { | ||
return cachedRgba | ||
} else { | ||
const rgba = normalizeColor.call(null, cssColor, "float32") | ||
this._cache.set(cssColor, rgba) | ||
return rgba | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface IColorNormalizer { | ||
normalizeColor(cssColor: string): Float32Array | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
const defaultTextureSizeInPixels = 512 | ||
const glyphPaddingInPixels = 0 | ||
|
||
export interface IWebGLAtlasOptions { | ||
fontFamily: string | ||
fontSize: string | ||
lineHeightInPixels: number | ||
linePaddingInPixels: number | ||
devicePixelRatio: number | ||
offsetGlyphVariantCount: number | ||
} | ||
|
||
export interface WebGLGlyph { | ||
width: number | ||
height: number | ||
textureWidth: number | ||
textureHeight: number | ||
textureU: number | ||
textureV: number | ||
variantOffset: number | ||
subpixelWidth: number | ||
} | ||
|
||
export class WebGLAtlas { | ||
private _glyphContext: CanvasRenderingContext2D | ||
private _glyphs = new Map<string, Map<number, WebGLGlyph>>() | ||
private _nextX = 0 | ||
private _nextY = 0 | ||
private _textureChangedSinceLastUpload = false | ||
private _texture: WebGLTexture | ||
private _textureSize: number | ||
private _uvScale: number | ||
|
||
constructor(private _gl: WebGL2RenderingContext, private _options: IWebGLAtlasOptions) { | ||
this._textureSize = defaultTextureSizeInPixels * _options.devicePixelRatio | ||
this._uvScale = 1 / this._textureSize | ||
|
||
const glyphCanvas = document.createElement("canvas") | ||
glyphCanvas.width = this._textureSize | ||
glyphCanvas.height = this._textureSize | ||
this._glyphContext = glyphCanvas.getContext("2d", { alpha: false }) | ||
this._glyphContext.font = `${this._options.fontSize} ${this._options.fontFamily}` | ||
this._glyphContext.fillStyle = "white" | ||
this._glyphContext.textBaseline = "top" | ||
this._glyphContext.scale(_options.devicePixelRatio, _options.devicePixelRatio) | ||
this._glyphContext.imageSmoothingEnabled = false | ||
|
||
this._texture = _gl.createTexture() | ||
_gl.bindTexture(_gl.TEXTURE_2D, this._texture) | ||
_gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, _gl.LINEAR) | ||
_gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, _gl.CLAMP_TO_EDGE) | ||
_gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, _gl.CLAMP_TO_EDGE) | ||
this._textureChangedSinceLastUpload = true | ||
this.uploadTexture() | ||
} | ||
|
||
public getGlyph(text: string, variantIndex: number) { | ||
let glyphVariants = this._glyphs.get(text) | ||
if (!glyphVariants) { | ||
glyphVariants = new Map() | ||
this._glyphs.set(text, glyphVariants) | ||
} | ||
|
||
let glyph = glyphVariants.get(variantIndex) | ||
if (!glyph) { | ||
glyph = this._rasterizeGlyph(text, variantIndex) | ||
glyphVariants.set(variantIndex, glyph) | ||
} | ||
|
||
return glyph | ||
} | ||
|
||
public uploadTexture() { | ||
if (this._textureChangedSinceLastUpload) { | ||
this._gl.texImage2D( | ||
this._gl.TEXTURE_2D, | ||
0, | ||
this._gl.RGBA, | ||
this._textureSize, | ||
this._textureSize, | ||
0, | ||
this._gl.RGBA, | ||
this._gl.UNSIGNED_BYTE, | ||
this._glyphContext.canvas, | ||
) | ||
this._textureChangedSinceLastUpload = false | ||
} | ||
} | ||
|
||
private _rasterizeGlyph(text: string, variantIndex: number) { | ||
this._textureChangedSinceLastUpload = true | ||
|
||
const { | ||
devicePixelRatio, | ||
lineHeightInPixels, | ||
linePaddingInPixels, | ||
offsetGlyphVariantCount, | ||
} = this._options | ||
const variantOffset = variantIndex / offsetGlyphVariantCount | ||
|
||
const height = lineHeightInPixels | ||
const { width: subpixelWidth } = this._glyphContext.measureText(text) | ||
const width = Math.ceil(variantOffset) + Math.ceil(subpixelWidth) | ||
|
||
if ((this._nextX + width) * devicePixelRatio > this._textureSize) { | ||
this._nextX = 0 | ||
this._nextY = Math.ceil(this._nextY + height + glyphPaddingInPixels) | ||
} | ||
|
||
if ((this._nextY + height) * devicePixelRatio > this._textureSize) { | ||
// TODO implement a fallback instead of just throwing | ||
throw new Error("Texture is too small") | ||
} | ||
|
||
const x = this._nextX | ||
const y = this._nextY | ||
this._glyphContext.fillText(text, x + variantOffset, y + linePaddingInPixels / 2) | ||
this._nextX += width | ||
|
||
return { | ||
textureU: x * devicePixelRatio * this._uvScale, | ||
textureV: y * devicePixelRatio * this._uvScale, | ||
textureWidth: width * devicePixelRatio * this._uvScale, | ||
textureHeight: height * devicePixelRatio * this._uvScale, | ||
width: width * devicePixelRatio, | ||
height: height * devicePixelRatio, | ||
subpixelWidth: subpixelWidth * devicePixelRatio, | ||
variantOffset, | ||
} as WebGLGlyph | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { INeovimRenderer } from ".." | ||
import { IScreen } from "../../neovim" | ||
import { CachedColorNormalizer } from "./CachedColorNormalizer" | ||
import { IColorNormalizer } from "./IColorNormalizer" | ||
import { IWebGLAtlasOptions } from "./WebGLAtlas" | ||
import { WebGLSolidRenderer } from "./WebGLSolidRenderer" | ||
import { WebGlTextRenderer } from "./WebGLTextRenderer" | ||
|
||
export class WebGLRenderer implements INeovimRenderer { | ||
private _editorElement: HTMLElement | ||
private _colorNormalizer: IColorNormalizer | ||
private _previousAtlasOptions: IWebGLAtlasOptions | ||
|
||
private _gl: WebGL2RenderingContext | ||
private _solidRenderer: WebGLSolidRenderer | ||
private _textRenderer: WebGlTextRenderer | ||
|
||
public start(editorElement: HTMLElement): void { | ||
this._editorElement = editorElement | ||
this._colorNormalizer = new CachedColorNormalizer() | ||
|
||
const canvasElement = document.createElement("canvas") | ||
canvasElement.style.width = `100%` | ||
canvasElement.style.height = `100%` | ||
|
||
this._editorElement.innerHTML = "" | ||
this._editorElement.appendChild(canvasElement) | ||
|
||
this._gl = canvasElement.getContext("webgl2") as WebGL2RenderingContext | ||
} | ||
|
||
public redrawAll(screenInfo: IScreen): void { | ||
this._updateCanvasDimensions() | ||
this._createNewRendererIfRequired(screenInfo) | ||
this._clear(screenInfo.backgroundColor) | ||
this._draw(screenInfo) | ||
} | ||
|
||
public draw(screenInfo: IScreen): void { | ||
this.redrawAll(screenInfo) | ||
} | ||
|
||
public onAction(action: any): void { | ||
// do nothing | ||
} | ||
|
||
private _updateCanvasDimensions() { | ||
const devicePixelRatio = window.devicePixelRatio | ||
this._gl.canvas.width = this._editorElement.offsetWidth * devicePixelRatio | ||
this._gl.canvas.height = this._editorElement.offsetHeight * devicePixelRatio | ||
} | ||
|
||
private _createNewRendererIfRequired({ | ||
width: columnCount, | ||
height: rowCount, | ||
fontWidthInPixels, | ||
fontHeightInPixels, | ||
linePaddingInPixels, | ||
fontFamily, | ||
fontSize, | ||
}: IScreen) { | ||
const devicePixelRatio = window.devicePixelRatio | ||
const offsetGlyphVariantCount = Math.max(4 / devicePixelRatio, 1) | ||
const atlasOptions = { | ||
fontFamily, | ||
fontSize, | ||
lineHeightInPixels: fontHeightInPixels, | ||
linePaddingInPixels, | ||
devicePixelRatio, | ||
offsetGlyphVariantCount, | ||
} as IWebGLAtlasOptions | ||
|
||
if ( | ||
!this._solidRenderer || | ||
!this._textRenderer || | ||
!this._previousAtlasOptions || | ||
!isShallowEqual(this._previousAtlasOptions, atlasOptions) | ||
) { | ||
this._solidRenderer = new WebGLSolidRenderer( | ||
this._gl, | ||
this._colorNormalizer, | ||
atlasOptions.devicePixelRatio, | ||
) | ||
this._textRenderer = new WebGlTextRenderer( | ||
this._gl, | ||
this._colorNormalizer, | ||
atlasOptions, | ||
) | ||
this._previousAtlasOptions = atlasOptions | ||
} | ||
} | ||
|
||
private _clear(backgroundColor: string) { | ||
const backgroundColorToUse = backgroundColor || "black" | ||
const normalizedBackgroundColor = this._colorNormalizer.normalizeColor(backgroundColorToUse) | ||
this._gl.clearColor( | ||
normalizedBackgroundColor[0], | ||
normalizedBackgroundColor[1], | ||
normalizedBackgroundColor[2], | ||
normalizedBackgroundColor[3], | ||
) | ||
this._gl.clear(this._gl.COLOR_BUFFER_BIT) | ||
} | ||
|
||
private _draw({ | ||
width: columnCount, | ||
height: rowCount, | ||
fontWidthInPixels, | ||
fontHeightInPixels, | ||
getCell, | ||
foregroundColor, | ||
backgroundColor, | ||
}: IScreen) { | ||
const canvasWidth = this._gl.canvas.width | ||
const canvasHeight = this._gl.canvas.height | ||
const viewportScaleX = 2 / canvasWidth | ||
const viewportScaleY = -2 / canvasHeight | ||
this._gl.viewport(0, 0, canvasWidth, canvasHeight) | ||
|
||
this._solidRenderer.draw( | ||
columnCount, | ||
rowCount, | ||
getCell, | ||
fontWidthInPixels, | ||
fontHeightInPixels, | ||
backgroundColor, | ||
viewportScaleX, | ||
viewportScaleY, | ||
) | ||
this._textRenderer.draw( | ||
columnCount, | ||
rowCount, | ||
getCell, | ||
fontWidthInPixels, | ||
fontHeightInPixels, | ||
foregroundColor, | ||
viewportScaleX, | ||
viewportScaleY, | ||
) | ||
} | ||
} | ||
|
||
function isShallowEqual<T>(objectA: T, objectB: T) { | ||
for (const key in objectA) { | ||
if (!(key in objectB) || objectA[key] !== objectB[key]) { | ||
return false | ||
} | ||
} | ||
|
||
for (const key in objectB) { | ||
if (!(key in objectA) || objectA[key] !== objectB[key]) { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for refactoring this! Definitely helped make it easier to understand 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy that it helped! I think I'll try to make it even more concise but that depends on how much time I will find for doing that. :)