Repository navigation
perf(rendering): rework the layout flow so resizes match fresh renders at half the time and memory - #2997
Merged
Conversation
…duration reference per layout Adds the layout-flow harness (resize-vs-fresh oracle, benchmarks, allocation and CPU probes) and fixes the first causes of resize results that differ from a fresh render. - test(rendering): add layout-flow harness for resize equivalence and layout benchmarks - test(rendering): compare layout-flow svg output below pixel precision - test(rendering): spike header spacing cache key over the layout-flow corpus - test(rendering): report header glyph allocations in the header cache spike - fix(rendering): keep justified bar widths and reset overflows per layout pass - test(rendering): add allocation, heap census, CPU and glyph visit analysis to the layout-flow harness - refactor(rendering): space page and parchment bars against one duration reference per layout - fix(rendering): include the beat content in the bottom extent of bent beats - test(rendering): check header glyph geometry against the header cache key - test(rendering): create the layout-flow output folder before writing heap snapshots
…out temporary allocations - perf(rendering): create beat glyph collections on first use - perf(rendering): create bar renderer helper collections on first use - perf(rendering): avoid style scope allocations when painting unstyled elements - perf(rendering): paint staff lines without collecting gap arrays - perf(rendering): paint beam flags without creating flag glyphs - perf(rendering): rotate tie control points without tuple allocations - perf(rendering): compute child paint offsets once in hot glyph loops - perf(rendering): store skyline segments in parallel number arrays - test(rendering): keep the layout-flow stage classifier out of the C# and Kotlin test builds
…he bounds lookup with fewer allocations - perf(rendering): lay out bar headers from shared profiles and create their glyphs for painting - perf(rendering): resolve bounds lookups with a single map access - perf(rendering): sort bar and beat bounds only when out of order - perf(rendering): build voice bounds without map entry allocations - perf(rendering): build the bounds lookup at display scale
One break rule for initial layout, resize and partial updates; forced line breaks stay on their bar after a resize. - refactor(rendering): decide which bars fit a system before adding them - perf(rendering): cache the vertical note extent of beat containers - perf(rendering): measure the gradual bend label once per bend glyph - perf(rendering): finish beaming helpers only once per bar layout - test(rendering): probe the measure-and-attach layout stages and skip removed methods
…kyline allocations - perf(rendering): skip bar-local overlay rods and overflows when attaching a bar - perf(rendering): compute only the natural bar width when placing a bar - test(rendering): measure the worker bounds hand-off and a flat wire prototype - perf(rendering): index beat containers and beam helpers by voice and beat - perf(rendering): create beaming helper storage on demand - perf(rendering): allocate shared beat sizes only for new beat positions - perf(rendering): find tab note glyphs by scanning the chord - perf(rendering): keep reserved collision slots as number pairs - perf(rendering): create bar-local skylines on the first insert - perf(rendering): keep the note of each main note head in the chord note infos - perf(rendering): look up chord note head infos per bar renderer without string keys - perf(rendering): create note parentheses, fingering and accidental groups only when needed - perf(rendering): keep a single glyph list per beat glyph - perf(rendering): create the chord note info list with its first note head - perf(rendering): share one effect placement scratch per layout - perf(rendering): refill the systems of the previous pass on resize - perf(rendering): keep the systems of earlier resizes for later ones - perf(rendering): merge skylines only over the shifted window - perf(rendering): keep on-time positions per spring index for two forces - perf(rendering): collect slur continuations without a generator - fix(rendering): build the slur registry staff key outside RenderStaff - fix(rendering): pass every argument in the effect note head super call - perf(rendering): resolve the effects that can create glyphs once per staff - fix(rendering): keep the bar of bar bounds when transferring the bounds lookup
…flat buffer Also fixes the SVG character references of music symbols, the bar of transferred bar bounds and the horizontal bounds lookup built once per partial. - perf(svg): reuse the entity text of music font symbols - test(layout-flow): probe the cost of the worker JSON hand-offs - test(rendering): measure what lazy or compact bounds lookups could save - test(layout-flow): staff-line run cost and image comparison probes - fix(svg): terminate the character reference of single music symbols - test(rendering): sort the imports of the bounds lookup JSON test - fix(rendering): build the horizontal bounds lookup once instead of per partial - perf(rendering): create the note bounds list with its first entry - perf(rendering): hand the bounds lookup from the browser worker as a flat buffer - test(layout-flow): measure the flat bounds hand-off in the worker probes - test(rendering): sort the imports of the bounds wire test - fix(svg): write music symbol codes as integers on Kotlin
- fix(rendering): use the default staff-line colour for bar styles without a staff-line entry - perf(rendering): paint staff lines once per run of bars - fix(platform): skip rectangles without width in the GDI and Android canvases - test(rendering): cover staff-line colour runs, same-string tab holes and ties across partials - test(layout-flow): measure the staff-line run painter and summarise staff-line SVG per commit - test(rendering): update reference images for staff lines painted per run
…rate the midi once per score load - perf(rendering): share the active effects of a staff across the systems of a layout - fix(model): accept null colours when deserializing - fix(rendering): reserve beam overflow of numbered rest-only beaming helpers - test(layout-flow): surface layout errors in the harness and the oracle - fix(player): generate the midi once per score load and player change - fix(rendering): keep tab note geometry unchanged by painting
…bjects - build(serializer): emit web-only plain-object toJsObject beside toJson - build(serializer): regenerate serializers with toJsObject - perf(worker): send score and settings to the render worker as plain objects - test(model): compare the toJsObject serializers with the Map path - test(rendering): write the numbered rest beaming test without generated test cases - fix(worker): serialize the score through an instance method - test(rendering): use test constructs that transpile to C# and Kotlin
- test(rendering): add a paint output probe (SVG census, buffer copy volume, string-building variants) - test(layout-flow): probe for import, model, midi and synth costs - test(rendering): retained-memory probe and byte-level heap snapshot census - test(layout-flow): probe resize allocation, finalize call counts and geometry kept between widths - test(rendering): add glyph census and measure-stage profile probes - test(layout-flow): keep the paint output probe out of the C# and Kotlin builds
…t and synth allocations - perf(rendering): skip beaming helpers and stem slots on tab staves with hidden rhythm - perf(rendering): lay out note numbers once and cache text measurements per layout - perf(rendering): lighter effect band containers - fix(rendering): fill the effect band glyph slots without using the fill result - perf(synth): update the voice low-pass state in place - perf(core): release the loaded model on destroy - perf(synth): hand the midi to the synth worker as a flat transferable buffer - perf(rendering): stop retaining painted partials in the lazy-partial store - perf(rendering): memoise SVG attribute tails and append once per element - perf(rendering): build SVG output in a StringBuilder on C# and Kotlin - perf(rendering): clear resize scratch arrays with pop loops instead of splice - perf(rendering): sort effect bands with a static comparator and skip trivial sorts - perf(rendering): clear the staff shared layout data only when it holds entries - perf(rendering): initialise the skyline bounds fields - perf(rendering): use indexed loops in the per-system resize functions - perf(rendering): trim the staff system pool after a layout pass
…s far out of view - perf(worker): coalesce resize renders while one is in flight - perf(browser): release the DOM of partials far out of view - perf(player): post the score for rendering before generating its midi - perf(player): make the audio worklet position report interval configurable
…aner SVG - perf(rendering): write the worker bounds directly in the wire format - test(layout-flow): compare the bounds lookup in the resize oracle - test(layout-flow): add a drag-like width sequence to the resize oracle - perf(rendering): keep pooled systems assembled when a resize gives them the same bars - perf(rendering): compact the chord note-head records - perf(rendering): keep the main beat glyph only in its typed field - perf(rendering): place pre-note spacing as numbers and drop empty pre-notes - perf(rendering): paint a single regular barline without a child glyph - perf(rendering): share the beaming and effect band scratch per layout - perf(rendering): share staff effect band lists across systems and start glyph groups without a list - perf(svg): write SVG output without newlines between elements - perf(player): report the played position every 10 milliseconds by default - perf(rendering): compute tie geometry in a layout scratch array and keep the tie bounding box as numbers - perf(rendering): skip completed multi-system slurs when collecting continuations - perf(rendering): return a shared display record from StaffDisplayResolver.merge when nothing overrides - perf(rendering): store single beat bounds without a list in the bounds lookup - test(rendering): pass typed display records in the staff display resolver tests - fix(browser): keep beat highlights on partials rendered while highlighting - perf(svg): write black rects without a fill attribute
Brings in the develop fixes up to 3984b32 (1.9.0 hardening, beaming bounds of beats without notes, triplet feel/capo placement, MusicXML dynamics, synth and importer fixes, dependency bumps). Develop's behaviour fixes are ported into the reworked layout code (header profiles, per-layout effect placement, staff lines painted per run, array-based note head records, flat worker hand-offs). Reference images that both sides changed are regenerated from the merged code; notation-legend resize-sequence-500 follows develop's fresh layout, since develop's reference records a stale helper-glyph overflow after resizing from a wider width.
…converter - test(rendering): use transpilable constructs in the bar layouting info tests - docs(transpiler): list Float64Array as unsupported - test(rendering): name the fourth slur scale-down reference after its width - fix(kotlin): return only the removed elements from List.splice - fix(model): read fonts from plain JSON objects - fix(model): accept a null font when deserializing - fix(model): write typed arrays as JSON arrays in the JSON converter - test(rendering): add width-dependence probe for the glyph layer - fix(rendering): keep whammy bar helper notes stable across finalize passes - fix(rendering): restart the bounds lookup when a partial render lays out everything - test(layout-flow): compare the worker wire bounds in the resize oracle - test(rendering): check the bounds wire format on a small covering score set - fix(model): cast typed arrays through unknown for the C#/Kotlin type check
…one glyph list per effect band - perf(synth): write the SynthHelper formulas out in Voice.render - perf(rendering): use indexed loops in rarely optimised layout and paint functions - perf(rendering): keep one glyph list per effect band and create chain heads on demand
… percent correctly - fix(importer): read Guitar Pro 3-5 grace notes on drum tracks as percussion notes - test(exporter): round trip the dense Guitar Pro 4 score through alphaTex - fix(importer): read the Guitar Pro 5 note duration percent as little-endian double - fix(platform): run the IntersectionObserver polyfill check on scroll and resize - test(audio): make the backing track output tests independent of test order
…uilds - perf(build): inline enum member reads across modules in the library builds - perf(rendering): clear tie shapes, key overlay rods and keep ledger line trims without allocating - perf(rendering): keep tie control points as doubles and sort effect bands in place - fix(rendering): lay out bend helper note heads the same way on every call - test(rendering): avoid a narrowed nullable local in the bend helper layout test
…ble on an alphaTex round trip - fix(importer): keep empty Guitar Pro voices and their dynamics stable on an alphaTex round trip - fix(importer): place Guitar Pro 7-8 drum notes on strings of their staff - fix(importer): read Guitar Pro 3-5 notes without fingering on a hand as unknown finger
…nd outline bends with fewer skyline boxes - fix(rendering): apply the default colour for style elements without an entry when forced - test(rendering): cover whole rests on numbered staves visually - fix(rendering): write rests longer than a quarter as repeated 0 on numbered staves - perf(rendering): register bend and whammy outlines with fewer skyline boxes - test(rendering): name the skyline shape test suite apart from the class it tests
- feat(core): add the trace log level - fix(core): drop the stray null detail from debug log calls - refactor(core): move internal flow details from debug to trace - perf(player): build the position trace message only when trace is enabled - perf(build): strip trace logging from the library builds - test: restore the global log level after tests that turn logging off
… keep quoted names in minified bundles - fix(android): compare keys when deleting map and set entries - fix(cross): parse integers like JavaScript parseInt on .NET and Android - fix(build): keep quoted property names in the minified bundles - test(exporter): compare numbers set on one side only in the round-trip helper
…the voice style - perf(rendering): paint ties into a scratch array of the layout - perf(rendering): collect overlay rods with indexed loops - fix(rendering): colour stems, beams, flags, tuplets and effects with the voice style - fix(rendering): reset score elements to the score info colour on a null style entry - fix(rendering): reset track names to their own default colour on a null style entry - perf(rendering): split bend and whammy outlines into coarser skyline steps - fix(rendering): drop the empty space in front of bend helper note accidentals
…ack through generated object readers and writers - refactor(serializer): generate write and read methods against IObjectWriter and IObjectReader - perf(serializer): keep closures out of the generated property readers - feat(worker): optionally send the score to the rendering worker as BSON - feat(worker): add MessagePack as a second binary score format - refactor(serializer): build the JSON object trees with PlainObjectWriter - perf(worker): send scores to the rendering worker as MessagePack by default - refactor(worker): send scores to the rendering worker only as MessagePack - fix(worker): leave headroom in the buffer for the next binary score - test(layout-flow): probe the binary score contents and the settings sync per resize - test: read serialized score arrays as untyped lists in the snapshot printer
… in their style colours - fix(logging): drop the stray null detail from five warning and error messages - fix(webpack): finish the build when the asset copy cannot run - refactor(cross): parse numbers with the platform parsers on .NET and Android - fix(rendering): paint the middle pieces of multi-system slurs in their voice colour - fix(rendering): paint the system separator in the main glyph colour by default - fix(rendering): reset the score header tuning to the score info colour - fix(rendering): paint bends of several notes on the tab staff in the voice colour
- perf(worker): key the record fields of the binary score by generated ids - fix(worker): report errors while reading the score through the error event
…he tab staff - fix(rendering): merge and stack the bend labels of several notes on the tab staff
…and write fields with typed calls - fix(android): grow boolean lists and search and sort number lists like JavaScript arrays - refactor(serializer): keep the field keys of each class once, in a generated field table - refactor(worker): decode the remaining string keys of the binary score without a key cache - refactor(serializer): generate toJson/fromJson only for the classes tagged @json_root - refactor(serializer): read the fields of strict classes in the handler of read - perf(serializer): write each primitive, array and map field with one typed writer call
- test: drop the layout-flow harness
The images of all topic commits of this branch, committed once. They change for: - fix(rendering): keep justified bar widths and space bars against one duration reference per layout - refactor(rendering): decide which bars fit a system before adding them - perf(rendering): paint staff lines once per run of bars - fix(model): read fonts, null fonts and typed arrays through the JSON converter - fix(rendering): write longer rests as repeated 0 on numbered staves and outline bends with fewer skyline boxes - fix(rendering): colour stems, beams, flags, tuplets and effects with the voice style - fix(rendering): paint slurs, system separators, tunings and tab bends in their style colours - fix(rendering): merge and stack the bend labels of several notes on the tab staff
It served to verify the position update interval while it was developed and is not needed as a demo.
…writer Biome 2.6 (develop's dependency update) reports the NaN check `v !== v` (noSelfCompare) and the assignment inside the ASCII copy loop condition (noAssignInExpressions). Both are written out without changing the output.
6 of 9 tasks
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Note
AI-authored disclosure (
alphatab-ai-authored-v1)Portions of this content were authored by an AI agent. The agent has read
AGENTS.md and the human submitter accepts responsibility for
compliance with the rules in that document.
Issues
No dedicated issue: part of the 1.9 release preparation.
Proposed changes
Problem. A resize produced a different layout than a fresh render at the same width. Across 2,725 test
cases (109 scores × 5 variations × 5 widths), 40 % differed:
Layout and resize were also slow and allocation-heavy. On large scores, every step of a window drag allocated about 45 MB.
What changes. The page/parchment layout is now one flow:
Around that flow, rendering, the browser worker hand-off, the player and the serializers were reworked to avoid
work and allocations nobody needs.
Most of the bug fixes below came out of the resize-vs-fresh comparison and the visual review of the new output.
Layout and resize
Forced line breaks stay on their bar, and a resize gives the same result as a fresh render at that width.
This holds in all 2,725 comparison cases (line breaks, bar widths, system heights, glyph positions and bounds).
ever grew, so the result depended on the resize history.
longer depend on the neighbouring bars of the system.
avoid per-layout allocations.
that changed when painted. The whammy and stale bend-overflow cases are also present on develop today.
Rendering output (reference images updated in the last commit)
0(0 0,0 0 0 0) instead of the dashes of held notes.bends.
nullstyle entries reset score elements, track names and the header tuning to their own defaults. Thesystem separator uses the main glyph colour by default. Bar styles without a staff-line entry use the default
staff-line colour.
helper accidentals is gone.
fillattribute, single music symbols endwith
;(valid XML), and Kotlin writes symbol codes as integers.Browser, worker and player
Float64Arrayinstead of a nested Map tree.numeric field ids keep it at 1.6 MB for the dense test score; with string keys it would be 8.7 MB. Errors while
the worker reads a score now reach
api.error.documented on
IScoreRenderer.resizeRender.sheet stayed in memory after one scroll through it. Beat highlights survive that re-render.
renderer the score is posted for rendering before its midi is generated.
player.positionUpdateIntervalInMilliseconds, default 10 ms: the audio worklet reports the positionevery 10 ms instead of every 128-frame quantum.
0restores the previous behaviour.destroy()releases the loaded model. Painted partials are no longer retained in the lazy-partial store.Model, importers, platform and build
(canon.gp5 and five other files had broken values), and notes without fingering get "unknown finger".
JsonConverteracceptsnullcolours and fonts, reads fonts from plain JSON objects and writes typed arrays asJSON arrays.
findBeatsreturnedevery beat once per partial. A partial render that falls back to a full layout no longer duplicates systems in
the lookup.
LogLevel.Trace(Logger.trace). Internal flow details moved from debug to trace. Trace calls arestripped from the library builds.
keep_quotedmakesOttavia._8vaand the other underscore members defined again in the minified bundles.Kotlin
List.splicereturns only the removed elements, Android map/set deletes compare keys, and .NET/Androidparse numbers like JavaScript
parseInt(via the platform parsers).Measured result (dense test score, 216 bars × 20 staves, all tracks)
alphaTab.core.min.mjsgzipThe other test scores (canon, full-song, notation-legend, a GP7 file) show the same direction. On all of them,
initial render takes 33–48 % less time and resizes 47–63 % less.
Compared with 1.8.4, the release is faster as well: initial render −28…36 % and resize −13…39 % on real
songs, and allocations are about halved. The one exception is the small notation-legend score, where
both are on par.
Compatibility notes
flat bounds).
Checklist
toJson/fromJsonover the visual-test corpus.AI authorship disclosure
(
alphatab-ai-authored-v1) is present at the top of this body, and I havepersonally reviewed every change and can explain each one
Further details