openfl / lime

A foundational Haxe framework for cross-platform development
https://lime.openfl.org/
MIT License
761 stars 375 forks source link

Improve support for FreeType glyph rendering #368

Open vroad opened 9 years ago

vroad commented 9 years ago

This is the demo that renders fonts with FreeType on openfl-next. Text layout and rectangular packing is currently done on starling library though.

http://community.openfl.org/t/freetype-based-font-rendrer-for-opefl-next-and-starling-openfl/729

As I shown in this post, I have been trying to fix inefficiencies in my fork.

First of all, we should not think that all glyphs can be rendered at once, because Japanese and Chinese have many characters. Doing this may cause the app to freeze.

jgranick commented 9 years ago

You may have noticed (perhaps not) that I've been working on fonts recently. This is the big missing piece for OpenFL "next", and I'd love to see it not only implemented in a basic form, but to really see a solution that gives us strong control over fonts moving forward.

It's a bit messy, but here's a sample that uses it:

https://github.com/openfl/lime-samples/blob/master/TextRendering/Source/Main.hx

I broke the Arabic text, perhaps because I'm doing it all based on character codes, and I think it has a lot of non-printing characters, though it does address a number of points

vroad commented 9 years ago

Thanks for your hard work, but it still seems to have problems...

vroad commented 9 years ago

And my RenderToImage implementation returns glyph metrics as well, since I was not sure if it returns the same glyph advance when hinting is applied.

vroad commented 9 years ago

Official FreeType doc states about "4. The effects of grid-fitting". http://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html

vroad commented 9 years ago

Ah, for now We don't have a method for accessing kerning information without decomposing the font.

jgranick commented 9 years ago

I have found it difficult to work through this process -- it's not a simple task to come up with a good, simple (but flexible) API for this ;)

I felt that the font.renderGlyphs API helped because it gives you the power to render one letter only, or several. I think that (perhaps?) this could work better if you could pass an existing map of glyphs, like font.renderGlyphs (20, new GlyphSet ("more letters"), glyphs); where it could (theoretically) append the new glyphs to the existing buffer.

I had a similar idea with metrics vs. images, glyph.image and glyph.metrics are both null by default, but when you use either the method to render, or to get glyph metrics, it populates that field. If you could pass existing glyphs in, then it could help populate both (if someone desired) on the same object.

With Harfbuzz for complex text layout, I'm not sure if the metrics will be as important (since it access it itself) but on the flip side, maybe you need metrics but not the image.

I would like to modify the font.decompose method to accept a glyph range as well, maybe you only want to convert outlines for a few glyphs

vroad commented 9 years ago

If you use FreeType without harfbuzz, I think that it still makes sense to return glyph metrics with rendered images. It is redundant to call FT_Load_Glyph twice, since it loads glyph in the rendering process.

if you use harfbuzz for text layout, you only need to use FreeType as a font rasterizer, as you said.

Font.renderGlyphs automatically create glyph map for you, but is it really necessary? Shouldn't we let users do it by themselves?

jgranick commented 9 years ago

I was running into inefficiencies in the way this way that the Font render API was working, so I've just given it an overhaul.

Now, it is much simpler, everything works on the glyph index (instead of accepting "GlyphSet" objects). In order to get the list if glyph indices for a given string, you call Font.getGlyphs ("hello"); and it returns it back. Then we can do subsequent calls using the glyph index, much simpler :smile:

There is Font.renderGlyph to render one image only, and Font.renderGlyphs to render a sheet that contains all the glyphs you requested. Currently, it aligns it in a grid pattern, but I think you may be using a smarter algorithm for packing it, so perhaps we could implement something like that internally here for better sizing.

This is not exposed, but before, I was noticing that it would crash if I tried to process too many glyphs at once. It used to create anonymous objects per glyph, now I am using a binary format to populate a ByteArray no matter how many glyphs are rendered.

vroad commented 9 years ago

Good job! I think that we still have couple of things to consider though.

Including rendered image data in single ByteArray may not be good for performance, especially when you are rendering glyphs in big size. It's relying on ByteArray's resize ability, since we cannot predict how big the rendered image will be. It may need to copy entire memory block to another place, to make allocated memory contiguous.

If we separate image data from glyph data like this, we could get better performance.

On Node.js, you cannot construct UInt8Array with new UInt8Array(someByteArray), as we are using Node's Uint8Array directry on Node.js target. We could replace that with some linline function call, such as createUInt8Array(ba) or getUInt8Array(ba).

In my implementation, I was directly uploading to OpenGL texture from ByteArray without creating ImageBuffer or Image, but avoid creating these instances may not make huge performance difference.

vroad commented 9 years ago

No, appending header to each ByteArray is enough. So we only need to pass

As that data doesn't need to be resizable, we could use alloc_buffer instead of ByteArray for performance, but that may be not necessary either.

vroad commented 9 years ago

I meant "prepend", not append...

I wonder if this change still cause some native targets to crush.

In your samples, you are not setting GL_UNPACK_ALIGNMENT. Since this value is 4 by default, without setting this value(or making the width of alpha-only image mutiple of 4) uploading alpha-only texture will fail and result in corrupted texture content.

https://github.com/openfl/lime-samples/blob/66739ef724132b85c97e978478141a66f1538874/TextRendering/Source/Main.hx#L153

vroad commented 9 years ago

Well, I didn't know that Font.renderGlyphs() still uses power of two image... Then I would use Font.renderGlyph() instead, as renderGlyphs would not allow me to use custom glyph map.

vroad commented 9 years ago

Looks like ByteArray doesn't support offsetting without creating copy. UInt8Array does copy in its constructor, too.

Too many copies! Then separating image data from glyph data(width, height, x, y) would make more sense, as it reduces at least one copy.

jgranick commented 9 years ago

If it was sensible, we could consider separating the glyph info from the pixel data, have one big binary blob of only the glyph data, I'm not sure if that would be sensible to push into GL or if we should assume the one copy from the internal glyph render to generating the ImageBuffer instance

I'm not sure that ByteArray is as optimized as it should be, I definitely think the typed arrays should be improved. I had an implementation going that used a "DataView" class, and all the others (UInt8Array, Int32Array, etc) were abstracts over it. This allowed changes between one type of view to another without a cost. These are all interfaces over a single piece of data, so it should not be so expensive. It was about 2-3 times faster if I recall

vroad commented 9 years ago

I tried that by myself. On hxcpp and nodejs, TextRendering example didn't crush. On neko, app crashes in somewhere in the code. It doesn't matter how many number of letters I try to render, strange... Isn't this neko's bug? https://github.com/vroad/lime/commit/c41917519445198dc9a6c09a2bb9144c516a23c4

vroad commented 9 years ago

I reverted my changes, rebuild tools and ndll, but neko still crashes. Why??

jgranick commented 9 years ago

I noticed that Neko works if I use only English text, I'm guessing this is something with Neko and unicode character encoding

larsiusprime commented 9 years ago

Neko has consistently in the past been unable to render characters that cpp & flash targets had no problem rendering, so I suspect that's a solid line of inquiry.

On Sat, Mar 14, 2015 at 1:28 PM, Joshua Granick notifications@github.com wrote:

I noticed that Neko works if I use only English text, I'm guessing this is something with Neko and unicode character encoding

— Reply to this email directly or view it on GitHub https://github.com/openfl/lime/issues/368#issuecomment-80656942.

www.fortressofdoors.com -- Games, Art, Design

vroad commented 9 years ago

Even with latest development version of neko, TextRendering still crashes. This shoud be reported as a bug.

vroad commented 9 years ago

You fixed something in the latest version? Neko no longer crashes with current lime.

vroad commented 9 years ago

I still have doubt about API design of Font.renderGlyphs... It's not friendly to texture caching.

vroad commented 9 years ago

I think that we should have separate classes for bin packing. If you want single image that can be uploaded with glTexImage2D, you should create it manually by converting result images.

jgranick commented 9 years ago

Hey! Yeah! It does work in Neko. I'm not sure what changed to do that, but I'm happy for it.

I'm open to improving the Font API, though I'm aware that Lime shouldn't be too high level -- you're right -- I think there are smarter ways to pack textures (although I'm not sure if we should do multiple font sizes in the same atlas, unless we overwrite them? Just concerned about the ability to dump unused symbols later). The packing should probably occur in a higher level than Lime.

I'm not sure performance-wise if it would be better to call renderGlyph repeatedly or renderGlyphs and copy into a new texture

vroad commented 9 years ago

I modified lime and lime-samples with techniques I described above. It only supports one texture cache for now(since it was difficult to implement QuadBatch by myself), but still tries to render glyphs from one texture as much as possible.

https://github.com/vroad/lime-samples/commit/7c8c263d8d3f56ca5c1b606fa18b14b114dafd21 https://github.com/vroad/lime/commit/5b4965bd72850b62b64aa25f780cd7c32ad0924f

vroad commented 9 years ago

Do we really need to have a function that renders only one glyph? We should be able to do the same thing with Font.renderGlyphs, and it should be rare to render only one glyph alone. Whether it is significant or not, Font.renderGlyphs should have less overhead than multiple Font.renderGlyph.

There are still number of things to consider...

In the case of texture deletion, we should render TextField to another texture with FBOs, maybe.

Sadly neko crashes again after applying my changes, even though cpp and nodejs works without problems.

vroad commented 9 years ago

lime_gl_tex_sub_image_2d had the problem with null value. Now new TextRendering example should work on all native targets. https://github.com/openfl/lime/pull/400

vroad commented 9 years ago

Was the use of anonymous objects really cause of neko crash? I doubt it, because you didn't show where neko crashed and what fixed the problem.

I created a new pull request for it and few other improvements. https://github.com/openfl/lime/pull/419

vroad commented 9 years ago

We could keep using maps for storing rendered glyphs, though bin packing should get separated from renderGlyphs, shouldn't it?