lenmus / lomse

A C++ library for rendering, editing and playing back music scores.
MIT License
117 stars 28 forks source link

Partial View for rendering systems individually #376

Closed npiegdon closed 1 year ago

npiegdon commented 1 year ago

This is conceptually similar to the Free Flow View, but would allow drawing one system at a time. (I also couldn't think of a very good name for it.) :sweat_smile:

Motivation: When using hardware accelerated drawing (3D APIs like DirectX, OpenGL, etc.), it is best to upload a rarely-changing texture only once and then use it repeatedly. (This assumes the app will be controlling which part of that texture is drawn each frame, effectively managing the viewport itself.)

If that were the only limitation, Free Flow View would be the right answer: render the whole score for a given screen width, upload it to the GPU, and draw portions as needed. But some mobile platforms still have the unfortunate combination of high DPI screens and rather limited GPU memory. So, a score with many pages, rendered at high DPI, all at once as a huge bitmap would immediately crash the app. Instead, more manual viewport management can be introduced to only render and upload the systems that are currently in the viewport. (An app could build arbitrarily complex caching systems on top of this basic single-system capability to do things like evict the least-recently-used systems as video memory pressure begins to ramp up, solving the problem.)

Because so much is being managed by the app manually, it will be important to be able to get at some information (horizontal begin/end pixels of each measure/beat for hardware-accelerated highlighting, etc.) that appears to normally be handled internally to lomse.

API: There are probably lots of ways this could work, but the API that feels most natural to me is something like the following.

int Paginate(width, DPI)

After creating the view, you could call something like Prepare or Paginate with width and DPI parameters. This function would get everything ready internally (clefs/key signatures at the beginning of each system, measure justification/spacing for the given width, slurs carried across line changes, etc.) the way Free Flow View does today. It returns the number of systems in the score. (My background is in less sophisticated sheet music rendering. Perhaps it isn't "number of systems" but rather "number of block elements in the document"?)

Paginate might be called again when the window size changes or the user requests a change to the dpi/zoom/size of the music, but it's a rare enough event that as much of the work should be done here up-front as possible.

After a Paginate, given an index (between 0 and the number returned by Paginate), at least this information should be available at this point (generally in units of pixels):

(Again, the presence of non-system block elements like paragraphs might make this a little messier. Using sentinel values like returning a measure range of zero to zero for the song title block could probably work. The measure/beat metrics would simply be an empty vector, etc.)

Using the system height, the set_rendering_buffer call can now be made with an appropriately-sized buffer. Assuming the height of the tallest system is similar to the average, you could get away with setting the buffer only once to the largest required for any system in the score.

Finally, redraw_bitmap would work as-usual but take a system index as a parameter. Only that system would be drawn at the top-left corner of the buffer (the way Free Flow View draws its first system).

As the music is scrolled or repeated or navigated by the user, many calls to redraw_bitmap(systemId) might be made (as systems are evicted from GPU memory and need to be redrawn) for any single call to Paginate. So ideally redraw_bitmap would need to do as little work as possible.

This relatively simple scheme allows for using lomse in a hardware accelerated environment while tackling the primary limitations. It does leave viewport management up to the app developer... but that's the part I already have written, so that works for me. :rofl: