googlefonts / oxidize

Notes on moving tools and libraries to Rust.
Apache License 2.0
173 stars 7 forks source link

preliminary notes #1

Closed cmyr closed 2 years ago

cmyr commented 2 years ago

Just taking a look at the current readme, and want to add my thoughts:

Wherein we contemplate moving shaping, rasterization, font compilation, and general font inspection and manipulation from Python & C++ to Rust.

To make sure I understand the scope/vision here, is the idea to have a single set of tools that would both be used in the font development/delivery pipeline, but which would also be eventually running on device? As in, are we imagining a set of tools that is a successor to both fontmake and HarfBuzz?

A bunch of other thoughts, copied over from my notes: this is pretty unstructured, but hopefully a starting point for some discussion:

other thoughts/Qs:

rsheeter commented 2 years ago

are we imagining a set of tools that is a successor to both fontmake and HarfBuzz?

Yes. Hopefully with incremental progression as parts of fontmake/fonttools and HB move across.

there is a fundamental tension between these two needs

Vigorously agree :) Threading the needle there is perhaps the most interesting problem here.

a set of low-level types that are high-fidelity representations of the types described in the spec and are zero-allocation, and on top of these we can have more expressive/expensive representations

A PoC or notes on what this would actually look like would be great. IMO allocating a pretty healthy chunk of time just to play with different patterns would be a good investment.

Once we have a PoC we think we like I imagine we can test-implement subsets of the problems against it. Maybe we port Swash to it, implement some subset of the subsetting task, and some subset of compilation?

a better overall understanding of the different tasks these tools would be performing

Conveniently we have access to shaping, subsetting, and rasterization wizards :) Reminds me, I haven't booked a kickoff meeting yet.

cmyr commented 2 years ago

Okay, I've had some time to think about this, and have some thoughts I'll write up soon. Chad has a WIP OpenType parser suitable for shaping called pinot. It is designed to work with bytes in memory, but I was able to get it working with mmap quite easily here:

https://github.com/cmyr/pinot/pull/1/files#diff-b614ab26b2d927d9785078575241ca0080aebde51d7317de54d96d2a030930f2

And on the other side, simon has been playing around with a library that does both parsing and writing, but with less concern about being suitable for the shaping case. It does use macros to generate types for tables, though:

https://github.com/simoncozens/fonttools-rs/blob/a7c64517a1a6cea100e2208126d50513726e48ef/crates/otspec/src/tables/head.rs#L5-L24

I think there is a chance to thread the needle here, a bit.

I'll write up my thoughts in more detail soon, but what I am picturing is a low-level crate that uses proc macros to generate zero-copy views into byte slices, that look a lot like chad's pinot code. Importantly, these types will have copy-on-write semantics, so that you can create owned versions when you're compiling.

Importantly, these will be byte-for-byte equivalent to the definitions in the OpenType spec.

On top of this, we can build other stuff, like our compiler. This can be much more heavy-weight, use high-information representations of things, and then call down into the lower-level crate only when it needs to generate the final binary representations.

rsheeter commented 2 years ago

Awesome! Any thoughts on ballpark time to 1) write up and 2) proof of concept? - no need to rush, don't interpret the question as pressure to sprint.

cmyr commented 2 years ago

I'll write up a one-pager today, and will try to come up with a reasonable time estimate. I think to feel confident about the approach I will want to implement some of the trickier tables (GPOS or GSUB at least, not sure if there are other good candidates?) and make sure there aren't any issues I haven't considered.

rsheeter commented 2 years ago

cmap format 4 is also a fun one. A thought: if you do cmap, glyf/loca, and GSUB/GPOS the PoC can demonstrate a partial subsetter and/or a partial shaper which could be an amusing exercise. @behdad might have additional suggestions for horrid tables.

behdad commented 2 years ago

cmap4 is an exception, so might not be the best example for a proof of concept.

GSUB/GPOS is big enough that if you do that you're a good percentage done :))).

dfrg commented 2 years ago

I thought fvar would be a nice representative test since it is relatively simple but deals with variable offsets and dynamically sized structures-- both difficult to generate.

behdad commented 2 years ago

I thought fvar would be a nice representative test since it is relatively simple but deals with variable offsets and dynamically sized structures-- both difficult to generate.

Indeed.

dfrg commented 2 years ago

Rod previously expressed interest in a more HarfBuzz style design based on the zerocopy crate and I started hacking on this a bit last week. I just finished up a quick and dirty but complete (read+write) implementation of fvar that can be found here: https://github.com/dfrg/pinot/blob/eff5239018ca50290fb890a84da3dd51505da364/src/fvar1.rs

cmyr commented 2 years ago

so I had thought about this a bit and it doesn't seem like zerocopy is really useful for our needs? The issue as I understand it is around endianness. The rationale for crates like zerocopy/bytemuck (and I'm more familiar with bytemuck) is that they enable safe abstractions for what is essentially transmutes of slices of bytes into &T; but since we always need to copy in order to handle endianness, we can't really take advantage of this?

Taking a look at your code, it looks like you're mostly using the AsBytes derive macro to generate a POD from the table fields? I do agree that something like this makes sense, but if we had some macro of our own I think the additional control over the generated code will likely be helpful in the more complicated cases? At least that's my current thinking.

dfrg commented 2 years ago

The AsBytes derive macro seems to just generate a trait impl for the trivial &T to &[u8] conversion and I don't believe it emits any new types. The endian swapping occurs at the read/write sites and the underlying memory is the raw font data. You could actually remove all those derives from the code and replace the as_bytes() calls with calls to a simple one liner function. I really only make use of the auto-endian-swapping primitive types.

I do agree it makes sense to define our own types and macros that we can adjust as necessary to match the problem space.

The key takeaway here (see my zero copy response in the PR) is that whether generated by macros or hand written, we may want to consider structures that directly alias memory (where possible) rather than pinot style wrappers that do dynamic loads from a byte slice for every field access. HarfBuzz does direct aliasing, so I assume Behdad can offer a lot of insight on this at the meeting.

rsheeter commented 2 years ago

I would definitely like us to explore direct aliasing for the read case. HarfBuzz is, afaik, best in class here and worthy of both study and benchmarking against.

cmyr commented 2 years ago

will close this, and we can open issues for particular discussions as needed.