Skip to content
This repository has been archived by the owner on Apr 1, 2020. It is now read-only.

Add experimental WebGL Renderer #2120

Merged
merged 9 commits into from Apr 25, 2018

Conversation

manu-unter
Copy link
Collaborator

@manu-unter manu-unter commented Apr 19, 2018

This PR is not ready to be merged yet, I will extend the description once it is. Consider this a sneak peek into my work in progress for now

For anyone feeling adventurous until then: The renderer should be working roughly now, with bold, italics and underline missing, and no performance optimizations or nice refactorings made so far.

This PR adds an experimental new renderer as an alternative to the current canvas-based one. I am doing this with the goal of solving several issues that we currently have with the optimizations done in the canvas renderer. First and foremost #1252 and #1083 should be fixed.

This PR intentionally does not cover all the functionality that we have in the canvas renderer yet, I am planning to add all the missing pieces with several smaller PRs afterwards. See the comments below for details in case you want to jump on the train!

@bryphe
Copy link
Member

bryphe commented Apr 20, 2018

@cryza , this is awesome! I just had a go with it and it's really great work.

I didn't realize how far along this was based on your description... When I ran it, I thought it would be obvious that it was different, but it looked basically the same on my machine (with no cut-off characters, though!) I had to open the debugger to make sure I was actually running the new code. 😄

Not only that, the performance was really impressive in my quick testing... I did some simple benchmarks of just navigating to the top/bottom of a file (triggering a full redraw).

For comparison, this is the old timing with the CanvasRenderer:
image

And this was the new timing with the WebGLRenderer:
image

Going from 22 seconds to 2 seconds for that benchmark is an incredible performance improvement. 💯

Thank you for sharing it out! Hope you don't mind if I post a couple questions, just so I can learn more how it works. 😄

0,
gl.RGBA,
gl.UNSIGNED_BYTE,
this.glyphCanvas,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool - if I understand correctly, this is how we can take our 'Canvas' that we render characters / glyphs to, and supply it as a texture that we can read from the shaders?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly what we're doing here :) This way we get the nice font rendering from Chrome while at the same time being able to run our editor through WebGL.

The only trade-off is that we need to manually manage our subpixel offsets: We need to manually manage the kerning.

Without additional effort, there is only one version of a character in the atlas, typically aligned to the full pixel boundary. With non-integer character widths however, this would probably ruin the kerning in many cases. A character might at some point need to start at 0.7 pixels offset within the pixel grid. To approximate the correct position, we have different variants of each rendered character in the atlas, each offset by a different fraction of a full pixel. Then we use the one that is closest to what we need at the given position in the editor panel. The number of different variants is controlled by subpixelDivisor in this file.

This idea was completely stolen from xray, BTW! All credit goes to @nathansobo :)

)
this._gl.drawElementsInstanced(this._gl.TRIANGLES, 6, this._gl.UNSIGNED_BYTE, 0, glyphCount)

this._gl.useProgram(this.textBlendPass2Program)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was curious - are the two passes needed for subpixel anti-aliasing? Or for handling transparency / background?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-pass approach is required to do correct blending of subpixel-antialiased text onto our destination canvas. Basically we are abusing the RGB output channels of the first pass to do some intermediate calculations and blending with the destination for us which we then use to calculate the final result in the second pass.

I will try to find the article again that explained the equations in a nice way and post it here!

@manu-unter
Copy link
Collaborator Author

@bryphe thanks for the great feedback! I'm really happy that you like it :)

So I am planning to do a major refactoring of all the low-level WebGL code so that it is more understandable and encapsulated in a better way. I hope that the code will be more maintainable afterwards also for people who didn't do a deep dive into WebGL.

I also still see a few quirks and performance improvements we might want to do and possibly also an adaptation of the INeovimRenderer, but I think that most of them should come in a separate PR afterwards, just like the rendering of bold, italic and underlined characters (which might also not be too trivial as it requires enhancements to the atlas mapping).

What I would like to finish before we merge this PR:

  • Refactor WebGL code
  • Hide the WebGL renderer behind an experimental configuration flag
  • Fix vertical alignment of the text with the cursor (currently off by ~1px)
  • Include line padding (currently ignored, but still yields the same line height AFAIK? 🤔 )
  • Maybe even fix the vertical antialiasing issue from Text rendering blurry not retina. #2047 and bring the fix back to the CanvasRenderer

@bryphe
Copy link
Member

bryphe commented Apr 21, 2018

So I am planning to do a major refactoring of all the low-level WebGL code so that it is more understandable and encapsulated in a better way. I hope that the code will be more maintainable afterwards also for people who didn't do a deep dive into WebGL.

That sounds great! I like the idea of refactoring to the high-level constructs (will help me out too, I spend some time reading through WebGL 2 docs, but still not proficient yet)

I also still see a few quirks and performance improvements we might want to do and possibly also an adaptation of the INeovimRenderer, but I think that most of them should come in a separate PR afterwards, just like the rendering of bold, italic and underlined characters (which might also not be too trivial as it requires enhancements to the atlas mapping).

Your plan sounds perfect to me. Adding the experimental configuration flag sounds great - perhaps we could have something like editor.renderer with 'canvas' | 'webgl' as values. For now, 'canvas' will still be the default, but users can experiment with 'webgl' - and then eventaully once the remaining work comes in, we'll flip the default. Need to get some champagne ready for that.... that'll be a big day 😀 🎉

Include line padding (currently ignored, but still yields the same line height AFAIK? 🤔 )

Interesting - we actually do something kind of crazy and incorporate the line padding into the fontHeightInPixels here in NeovimInstance.ts:

    public setFont(fontFamily: string, fontSize: string, linePadding: number): void {
        this._fontFamily = fontFamily
        this._fontSize = fontSize

        const { width, height, isBoldAvailable, isItalicAvailable } = measureFont(
            this._fontFamily,
            this._fontSize,
        )

        this._fontWidthInPixels = width
        this._fontHeightInPixels = height + linePadding

This is confusing because the fontHeightInPixels that is everywhere isn't actually the measured pixel height of the font... its the measured pixel height of the font plus the line padding. This could potentially be causing problems.

Of course, keep me posted if there is anything I can do 👍 Awesome work @cryza !

@nathansobo
Copy link

@cryza thanks for the shout-out. I’d definitely appreciate a ping on PRs related to this code path so I can learn from your progress. Good luck.

package.json Outdated
@@ -750,6 +752,7 @@
"@types/sinon": "1.16.32",
"@types/webgl2": "^0.0.3",
"autoprefixer": "6.4.0",
"aws-sdk": "^2.202.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this dependency included ?

Copy link
Contributor

@DeltaEvo DeltaEvo Apr 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aw this is a merge from master, sorry 😂

@bryphe
Copy link
Member

bryphe commented Apr 24, 2018

Thanks for your help @nathansobo - look forward to seeing where xray goes! 👍 It's been exciting watching the progress.

const viewportScaleY = -2 / canvasHeight
this._gl.viewport(0, 0, canvasWidth, canvasHeight)

this._solidRenderer.draw(
Copy link
Member

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 👍

Copy link
Collaborator Author

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. :)

}

private createVertexArrayObject() {
this._vertexArrayObject = this._gl.createVertexArray()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was doing some research on WebGL today and stumbled across this library: https://github.com/regl-project/regl

I was wondering if you looked at it and what you thought about it? It seems like it might be able to cut out some of the boilerplate here - but I'm not sure if it exposes enough low-level functionality to handle all the cases we need.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I havent't seen that library before, and it looks really interesting! I actually really dislike the mutative and OO way of programming that the GL APIs enforce, so this hits the spot for me :)

Unfortunately though it seems like there is no WebGL2-compatible version of this library and I currently don't know if our shaders could be rewritten in a WebGL1-compatible way. I had a quick try and changed the typings of the contexts to WebGL1, which resulted in lots of errors. So my gut feeling for this is that it's not really possible, but one would need to do some more investigation into this to be sure.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebGL 1 doesn't support instanced rendering as far as we could tell, so it will be a non-trivial change and slightly less efficient to use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nathansobo Great to have you in the loop! Thanks for the input, that probably just saved us a lot of time :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cryza and @nathansobo ! That's a bummer that regl doesn't support WebGL 2 (I like the functional approach better too) - but definitely a non-starter if we'd have to switch to WebGL 1 👍 Good to know! Definitely saved us time. 😄

@bryphe
Copy link
Member

bryphe commented Apr 24, 2018

Refactoring looks great, @cryza ! I like how you split out the functionality between WebGLTextRenderer and WebGLSolidRenderer - made it much clearer for me.

@manu-unter
Copy link
Collaborator Author

manu-unter commented Apr 24, 2018

Okay, this is slowly coming into shape.

My progress thus far:
☑️ Refactor WebGL code (at least the first round)
☑️ Hide the WebGL renderer behind an experimental configuration flag
☑️ Fix vertical alignment of the text with the cursor (currently off by ~1px)
☑️ Include line padding (thanks for the help here @bryphe, this fixed the point above at the same time)
❌ Maybe even fix the vertical antialiasing issue from #2047 and bring the fix back to the CanvasRenderer - this seems to be much harder than I thought. Let's tackle it in the other issue :)

I still have the idea to combine the two populate... methods from the text and the solid renderer into one loop in the top-level renderer, which might add some more performance and also keep the IScreen out of the low-level renderers, making them fully specialized on WebGL, but that could also come later

Things that are still missing for feature parity with the canvas renderer (for other PRs):

  • bold, italic and underlined rendering (and combinations)
  • ligatures
  • proper error handling for the atlas in case it runs out of space (restart with double size?)
  • support for transparent editor background colors without rendering errors

Given that the builds complete, this would put the PR in a reviewable state :)

@parkerault
Copy link
Contributor

If there is an experimental flag for this, I would suggest merging it into master as soon as the refactoring is complete. I am very eager to try this out!

@codecov
Copy link

codecov bot commented Apr 24, 2018

Codecov Report

Merging #2120 into master will decrease coverage by 0.48%.
The diff coverage is 11.57%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2120      +/-   ##
==========================================
- Coverage   37.27%   36.79%   -0.49%     
==========================================
  Files         288      293       +5     
  Lines       11608    11957     +349     
  Branches     1556     1579      +23     
==========================================
+ Hits         4327     4399      +72     
- Misses       7037     7311     +274     
- Partials      244      247       +3
Impacted Files Coverage Δ
...src/Services/Configuration/DefaultConfiguration.ts 86.66% <ø> (ø) ⬆️
browser/src/neovim/Screen.ts 4.34% <0%> (-0.04%) ⬇️
browser/src/Renderer/WebGL/WebGLTextRenderer.ts 10.52% <10.52%> (ø)
browser/src/Renderer/WebGL/WebGLSolidRenderer.ts 11.59% <11.59%> (ø)
browser/src/Renderer/WebGL/WebGLUtilities.ts 21.21% <21.21%> (ø)
...rowser/src/Renderer/WebGL/CachedColorNormalizer.ts 22.22% <22.22%> (ø)
browser/src/Renderer/WebGL/WebGLAtlas.ts 5.55% <5.55%> (ø)
browser/src/Editor/NeovimEditor/NeovimEditor.tsx 9.39% <50%> (+0.17%) ⬆️
browser/src/Renderer/WebGL/WebGLRenderer.ts 8.69% <8.69%> (ø)
browser/src/Input/KeyBindings.ts 2.29% <0%> (-0.03%) ⬇️
... and 12 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 01b7ee0...04d5eaa. Read the comment docs.

@CrossR
Copy link
Member

CrossR commented Apr 24, 2018

Giving this a try out now and the performance for full-redraws seems great, and I don't seem to get any "cracks" anymore.

One thing I've noticed is that updating the screen in small ways, ie with /someSearchTerm removes everything except the highlights of the matches, so I'm assuming something isn't being fully rendered when search highlights are. Similarly if I scroll fast with jk or mouse wheel, I end up in the same state where only the mouse cursor shows the character.

In both cases, forcing a redraw with gg or G fixes it however.

@bryphe
Copy link
Member

bryphe commented Apr 25, 2018

I just went through the code again - @cryza , you did an awesome job refactoring and restructuring the code - it's very readable. I really like the way you architected this strategy 👍 And I tested it out locally and it works great. I'm looking forward to experimenting with it in my day-to-day work 😄

I see no reason to wait, so I'll bring this in now - it's gated by the 'editor.renderer' configuration setting, so we can continue to iterate on master and work through any outstanding bugs and bring it up to feature parity with our CanvasRenderer. It also means that this will be in our Friday release, which is awesome 💯 I know there are a lot of users excited to try this out!

@cryza - amazing work! Really impressed by this change - it's a huge contribution to the project - not only does it lay the foundation for fixing long-standing issues with our rendering pipeline like #2047, #1252, #1083, it also sets a new bar in terms of performance. Thank you! And thanks @nathansobo for your help!

Next step - get this enabled by default! 😀

@bryphe bryphe merged commit 479fd42 into onivim:master Apr 25, 2018
@adelarsq
Copy link

I don't know if its related but after this change shows this messages when using webgl:

Received notification for - Unhandled Exception:Error: Texture is too small
Please report this error. Callstack: Error: Texture is too small
    at Wi._rasterizeGlyph (file:///C:/dev/oni/lib/browser/vendor.bundle.js:964:15106)
    at Wi.getGlyph (file:///C:/dev/oni/lib/browser/vendor.bundle.js:964:14430)
    at Xi.populateGlyphInstances (file:///C:/dev/oni/lib/browser/vendor.bundle.js:964:19764)
    at Xi.draw (file:///C:/dev/oni/lib/browser/vendor.bundle.js:964:17781)
    at Ji._draw (file:///C:/dev/oni/lib/browser/vendor.bundle.js:964:22957)
    at Ji.redrawAll (file:///C:/dev/oni/lib/browser/vendor.bundle.js:964:21619)
    at br._onResize (file:///C:/dev/oni/lib/browser/vendor.bundle.js:791:3136)
    at ResizeObserver._boundOnResizeMethod._resizeObserver.window.ResizeObserver.e (file:///C:/dev/oni/lib/browser/vendor.bundle.js:791:2653)
Uncaught Error: Texture is too small
    at Wi._rasterizeGlyph (vendor.bundle.js:964)
    at Wi.getGlyph (vendor.bundle.js:964)
    at Xi.populateGlyphInstances (vendor.bundle.js:964)
    at Xi.draw (vendor.bundle.js:964)
    at Ji._draw (vendor.bundle.js:964)
    at Ji.redrawAll (vendor.bundle.js:964)
    at br._onResize (vendor.bundle.js:791)
    at ResizeObserver._boundOnResizeMethod._resizeObserver.window.ResizeObserver.e (vendor.bundle.js:791)

@manu-unter
Copy link
Collaborator Author

@adelarsq thanks for the report! This is caused by the webgl renderer as you assumed. It currently happens if the texture that contains all the prepared characters runs out of space. Currently we hard-coded the texture size to 512x512 pixels, so for files with lots of different characters, this limit will at some point cause the renderer to throw. Especially so for large font sizes.

My current idea is to implement some logic that catches the error at the level of the text renderer, creates a larger texture and restarts rendering.

To find a reasonable default size in any case, it would be great if you could share some information about your font size, font family, line padding, if you have a retina display (or some other setup with devicePixelRatio > 1) and ideally also the content of the file, if that is possible.

Cheers!

@CrossR
Copy link
Member

CrossR commented Apr 26, 2018

I also hit a similar issue when doing lots of scrolling very rapidly.

I'm using Fira Code as my font at 12px, with the default line padding and a 4k monitor (27" if that matters?) at 150% scaling, with Windows 10 64bit.

The file was my Oni config, which can be found here: https://github.com/CrossR/dotfiles/blob/master/oni/config.tsx

@manu-unter manu-unter deleted the cryza/performance/webgl-renderer branch May 9, 2018 15:43
@justinmk
Copy link

oni 0.3.4, macOS 10.13.4

Setting "editor.renderer": "webgl" gives me this error immediately on startup:

bundle.js:1 Received notification for - Unhandled Exception:TypeError: Cannot read property 'canvas' of null
Please report this error. Callstack: TypeError: Cannot read property 'canvas' of null
    at Ji._updateCanvasDimensions (file:///Users/justin/opt/Oni.app/Contents/Resources/app/lib/browser/vendor.bundle.js:964:21733)
    at Ji.redrawAll (file:///Users/justin/opt/Oni.app/Contents/Resources/app/lib/browser/vendor.bundle.js:964:21520)
    at eo._render (file:///Users/justin/opt/Oni.app/Contents/Resources/app/lib/browser/vendor.bundle.js:964:44256)
    at _pendingAnimationFrame.window.requestAnimationFrame (file:///Users/justin/opt/Oni.app/Contents/Resources/app/lib/browser/vendor.bundle.js:964:44126)
a @ bundle.js:1
vendor.bundle.js:964 Uncaught TypeError: Cannot read property 'canvas' of null
    at Ji._updateCanvasDimensions (vendor.bundle.js:964)
    at Ji.redrawAll (vendor.bundle.js:964)
    at eo._render (vendor.bundle.js:964)
    at _pendingAnimationFrame.window.requestAnimationFrame (vendor.bundle.js:964)

@manu-unter
Copy link
Collaborator Author

manu-unter commented May 13, 2018

@justinmk thanks for the report! That happens due to the fact that sometimes the drawing call can be triggered before the renderer was initialized. I will try to fix that in my current branch where I am improving the stability of the WebGL renderer. This is the PR: #2198

@badosu
Copy link
Collaborator

badosu commented May 16, 2018

@cryza Just tested master here, was not working on the latest stable (v0.3.4), it's fixed for me!

Looking forward to set webgl on my stable setup when it's released, thanks!!

@manu-unter
Copy link
Collaborator Author

Great to hear, @badosu! Thanks for giving it another try this quickly :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants