libass icon indicating copy to clipboard operation
libass copied to clipboard

Support Banner/Scroll effects’ fadeawaywidth/height gradient

Open astiob opened this issue 2 years ago • 6 comments

Guide to reviewing the commits:

  1. The first commit lays down some foundation and implements an integer-only gradient. If you’re not interested in the integer-only gradient specifically, feel free to skip the gradient arithmetic in your initial review.
  2. The third commit implements a mathematically sound floating-point gradient, the “sanest” variant. This can/will be squashed into the first commit if we settle on a floating-point variant, completely replacing the integer-only code.
  3. The fourth commit is a small but icky patch to get us closer to DirectShow VSFilter if we think that’s useful. We can decide to drop this.

There are several bikesheds to be coloured here:

  • Should the gradient be implemented as the splitting of ASS_Images with tweaked ASS_Image alphas (as the proposed code does) or by pre-multiplying the backing bitmap(s)?

    • VSFilter does the latter. But this is a scrolling event, so (unless it’s scrolling reaaaally slowly, which may be used to emulate a stationary gradient) this can’t be cached and the whole bitmap needs to be redone on each frame. Plus, we’d need either to allocate and fill a separate bitmap for the gradient to use mul_bitmaps on it (VSFilter does this) or introduce a special-purpose multiplier routine for this, which we’d probably like to have an assembler-language implementation for (which someone would have to create).

    • Splitting ASS_Images lets us forego any bitmap multiplications and allows bitmap reuse (though reuse can still be limited due to subpixel shifts during the scrolling), but it requires more ASS_Image allocations and breaks those API consumers that use ASS_Images to estimate event bounding boxes.

  • How stable should the output be on changing display resolution, and how close should it be to VSFilter’s?

    I initially offer three variants in this PR. One is a stupid simple implementation, much of the same kind of stupid as in VSFilter and matching it somewhat but not fully, and technically moving by sub-pixel amounts as display resolution is changed. Another is a mathematically ideal variant that to me is the most intuitive thing an author would expect from fadeawayheight/width without knowing how any implementation actually does it. The third is a mathematically exact replica of traditional VSFilter’s gradient position, but I didn’t replicate the bad rounding of values (which in some sense shifts the gradient further), so I’m not entirely sure how this even helps. I can add that if desired, though.

    I haven’t had much luck imagining situations where a carefully crafted script might be significantly affected by these differences. But I haven’t tried looking for real examples, either. Theoretically, a script might use these effects to achieve a colour gradient.

    For reference, VSFilter’s rounding errors on values:

    1. It computes the gradient step as floor(2¹⁴ / gradientWidthOrHeightInDisplayPixels) and repeatedly adds this (starting from fully transparent) for the leading gradient or subtracts this (starting from fully opaque) for the trailing gradient, accumulating/multiplying the error caused by the floor. As a result, the leading gradient never quite smoothly reaches 1, and the trailing gradient never quite smoothly reaches 0; and the higher the resolution, the worse it may get.

    2. It computes the final opacity as floor(opacity × gradient / 2¹⁴). And the opacities are in the range 0–64, so even this floor alone is worth up to ¹⁄₆₄ ≈ ⁴⁄₂₅₅. The leading gradient in particular might thus appear to originate another few pixels beyond the designated point.

  • I’ve noticed the rounding of \clip(,,,) coordinates is different among the various renderers. Do we or do we not want to align this to any particular VSFilter flavour, or perhaps independently improve our own?

astiob avatar Nov 17 '22 02:11 astiob

It appears this introduces (or triggers an existing) issue with state persisting beyond its intended lifetime. Here’s a sample:

[Script Info]
ScriptType: v4.00+
ScaledBorderAndShadow: yes
YCbCr Matrix: None
PlayResX: 1920
PlayResY: 1080

[Aegisub Project Garbage]
Video File: ?dummy:25.000000:39000:1920:1080:47:191:225:

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,DejaVu Sans,48,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,10,10,10,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:00:04.99,Default,,0,0,0,Banner;9;0;300,{\4c&HFF0000\1a&HFE\xshad-1250}{\p1}m 0 0 l 600 0 600 300 0 300
Dialogue: 0,0:00:05.00,0:00:08.00,Default,,0,0,0,,AAA{\1c&HFF0000}{\p1}m 0 0 l 600 0 600 300 0 300{\p0}AAAA

The second event doesn't render if the first one was displayed before it. If playback is started after the first event or the first even is commented the second event shows up normally.

TheOneric avatar Nov 17 '22 17:11 TheOneric

Thanks for testing! I had only tried single frames, as I don’t (yet) have an easily testable video playback setup. Try with the new code, please.

I’ve added a second condition to apply_scroll_fade. It appears we reset evt_type but not any of the scroll_... properties in init_render_context, which makes sense but I didn’t pay attention to.

astiob avatar Nov 17 '22 18:11 astiob

Try with the new code, please.

Both events show up now.

TheOneric avatar Nov 17 '22 18:11 TheOneric

I’m also not sure what ASS_ImagePriv->buffer even is.

Currently, an ASS_Image references a source bitmap: either a borrowed cache entry (in which case source is set and reference-counted while buffer is null) or a directly owned bitmap buffer (in which case source is null but buffer is this very buffer). The latter is used for BorderStyle=4 in add_background and for blended vector clips in blend_vector_clip; the former is used for everything else.

This adds a third mode, where the ASS_Image references a direct bitmap buffer that is borrowed from another ASS_Image that owns it. In this case, both source and buffer are null, and the owning image is in the same image list, so both images will be freed by ass_frame_unref at the same time.

astiob avatar Nov 19 '22 21:11 astiob

For all sane use cases lround = lrint, so I don't understand what are you meaning here.

I, in turn, don’t understand what you mean by sane use cases. I gave an example of what I mean in the same commit message. Consider a 720p video/script on a 1080p screen: the scaling factor is exactly 1.5. Whole pixels in the source turn into 1.5 display pixels each. lrint rounds successive 1px steps—1.5px steps after scaling—to intervals of 1px, 2px, 2px, 1px, 1px, 2px, 2px, 1px, 1px, 2px, 2px… lround, in contrast, gives 1px, 2px, 1px, 2px, 1px, 2px, 1px, 2px… which is much smoother. In fact, this applies not only to animations (as I wrote in the commit message) where this stepping happens over time, but also to those same insane typeset signs™ where it happens within the span of a single frame, with a separate event for each pixel.

astiob avatar Nov 20 '22 21:11 astiob

Pushed the non-fadeaway commits to master for now, with an expanded example regarding lrint vs lround.

astiob avatar Feb 09 '23 14:02 astiob