package ass import ( "fmt" "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/ass/line" "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/ass/tag" "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/settings" "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types" "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/math" "git.gammaspectra.live/WeebDataHoarder/swf2ass-go/types/shapes" "github.com/ctessum/polyclip-go" "runtime" "slices" "strconv" "sync" "sync/atomic" "time" ) type Renderer struct { Header []string RunningBuffer []*line.EventLine Display shapes.Rectangle[float64] Statistics map[uint16]rendererStatsEntry } func NewRenderer(title string, frameRate float64, display shapes.Rectangle[float64]) *Renderer { width := int64(display.Width() * settings.GlobalSettings.VideoScaleMultiplier) height := int64(display.Height() * settings.GlobalSettings.VideoScaleMultiplier) ar := float64(width) / float64(height) frameRate *= settings.GlobalSettings.VideoRateMultiplier return &Renderer{ Statistics: make(map[uint16]rendererStatsEntry), Display: display, Header: []string{ "[Script Info]", "; Script generated by swf2ass Renderer", "; https://git.gammaspectra.live/WeebDataHoarder/swf2ass-go", fmt.Sprintf("Title: %s", title), "ScriptType: v4.00+", //TODO: WrapStyle: 0 or 2? "WrapStyle: 2", "ScaledBorderAndShadow: yes", "YCbCr Matrix: PC.709", fmt.Sprintf("PlayResX: %d", width), fmt.Sprintf("PlayResY: %d", height), fmt.Sprintf("LayoutResX: %d", width), fmt.Sprintf("LayoutResY: %d", height), "", "", "[Aegisub Project Garbage]", "Last Style Storage: f", fmt.Sprintf("Video File: ?dummy:%s:10000:%d:%d:160:160:160:c", strconv.FormatFloat(frameRate, 'f', -1, 64), width, height), fmt.Sprintf("Video AR Value: %s", strconv.FormatFloat(ar, 'f', -1, 64)), "Active Line: 0", "Video Zoom Percent: 2.000000", "", "[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", fmt.Sprintf("Style: %s,Arial,20,&H00000000,&H00000000,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1", line.StyleFill), fmt.Sprintf("Style: %s,Arial,20,&H00000000,&H00000000,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0,0,0,1", line.StyleLine), "", "[Events]", "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text", }, } } func (r *Renderer) RenderFrame(frameInfo types.FrameInformation, frame types.RenderedFrame, keyframeInterval, flushInterval, flushCountLimit int) (result []string) { if len(r.Header) != 0 { result = append(result, r.Header...) r.Header = nil } slices.SortStableFunc(frame, RenderedObjectDepthSort) var linesKeptOrTransitioned []*line.EventLine var linesNotTransitioned []*line.EventLine scale := math.ScaleTransform(math.NewVector2(settings.GlobalSettings.VideoScaleMultiplier, settings.GlobalSettings.VideoScaleMultiplier)) animated := 0 viewPortClip := shapes.NewClipPath(r.Display.Draw()).ApplyMatrixTransform(scale, true) for _, object := range frame { obEntry := *BakeRenderedObjectsFillables(object) object = &obEntry if _, ok := r.Statistics[object.ObjectId]; !ok { r.Statistics[object.ObjectId] = rendererStatsEntry{ Frames: &atomic.Uint64{}, Lines: &atomic.Uint64{}, Size: &atomic.Uint64{}, } } object.MatrixTransform = scale.Multiply(object.MatrixTransform) //TODO: order? if object.Clip != nil { //apply scale, then clip to viewPort newClip := object.Clip.ApplyMatrixTransform(scale, true) newClipIntersected := newClip.Intersect(viewPortClip) if len(newClipIntersected.Clip.Pol) == 0 { //shape clip does not intersect with viewport! we can safely drop this continue } diffPol := viewPortClip.Clip.Pol.Construct(polyclip.DIFFERENCE, newClipIntersected.Clip.Pol) if len(diffPol) == 0 { //omit viewport that is exactly the display area object.Clip = nil } else { object.Clip = newClipIntersected } } depth := object.GetDepth() var tagsToTransition []*line.EventLine for i := len(r.RunningBuffer) - 1; i >= 0; i-- { tag := r.RunningBuffer[i] if depth.Equals(tag.Layer) && object.ObjectId == tag.ObjectId && (keyframeInterval == 0 || tag.GetDuration() < int64(keyframeInterval)) { tagsToTransition = append(tagsToTransition, tag) r.RunningBuffer = slices.Delete(r.RunningBuffer, i, i+1) } } slices.Reverse(tagsToTransition) canTransition := true var transitionedTags []*line.EventLine for _, tag := range tagsToTransition { tag = tag.Transition(frameInfo, object) if tag != nil { transitionedTags = append(transitionedTags, tag) tag.DropCache() } else { canTransition = false break } } r.Statistics[object.ObjectId].Frames.Add(1) if canTransition && len(transitionedTags) > 0 { animated += len(transitionedTags) linesKeptOrTransitioned = append(linesKeptOrTransitioned, transitionedTags...) } else { linesNotTransitioned = append(linesNotTransitioned, tagsToTransition...) for _, l := range line.EventLinesFromRenderObject(frameInfo, object, settings.GlobalSettings.ASSBakeMatrixTransforms) { l.DropCache() linesKeptOrTransitioned = append(linesKeptOrTransitioned, l) } } } //Flush tags that explicitly could not transition result = append(result, r.flush(frameInfo, linesNotTransitioned)...) var tagsToKeep []*line.EventLine if flushInterval > 0 { //loop things kept in buffer that could not transition, and see which must be flushed for i := len(r.RunningBuffer) - 1; i >= 0; i-- { eventLine := r.RunningBuffer[i] // keep tag for further transitions, BUT try to hide it if flushInterval == 0 || eventLine.GetSinceLastTransition() < int64(flushInterval) { eventLine := eventLine.TransitionVisible(frameInfo, false) if eventLine != nil { //we can transition! tagsToKeep = append(tagsToKeep, eventLine) r.RunningBuffer = slices.Delete(r.RunningBuffer, i, i+1) } } } slices.Reverse(tagsToKeep) if len(tagsToKeep) > flushCountLimit { slices.SortFunc(tagsToKeep, func(a, b *line.EventLine) int { return int(a.GetSinceLastTransition() - b.GetSinceLastTransition()) }) //append back tail of buffer r.RunningBuffer = append(r.RunningBuffer, tagsToKeep[flushCountLimit:]...) tagsToKeep = tagsToKeep[:flushCountLimit] } } fmt.Printf("[ASS] Total %d objects, %d flush, %d buffer, %d kept, %d animated tags.\n", len(frame), len(r.RunningBuffer)+len(linesNotTransitioned), len(linesKeptOrTransitioned), len(tagsToKeep), animated) linesKeptOrTransitioned = append(linesKeptOrTransitioned, tagsToKeep...) //Flush non dupes on Running buffer result = append(result, r.Flush(frameInfo)...) r.RunningBuffer = linesKeptOrTransitioned return result } func threadedRenderer(stats map[uint16]rendererStatsEntry, buf []*line.EventLine, duration time.Duration) []string { if len(buf) == 0 { return nil } results := make([]string, len(buf)) var cnt atomic.Uint64 var wg sync.WaitGroup for i := 0; i < min(len(buf), runtime.NumCPU()); i++ { wg.Add(1) go func(n int) { defer wg.Done() for { i := cnt.Add(1) - 1 if i >= uint64(len(buf)) { break } l := buf[i] var hasColor bool for _, t := range l.Tags { if ct, ok := t.(tag.ColorTag); ok && ct.HasColor() { hasColor = true break } } if !hasColor { //skip lines without any color at all continue } stats[l.ObjectId].Lines.Add(1) l.Name += fmt.Sprintf(" f:%d>%d~%d", l.Start, l.LastTransition, l.LastTransition-l.Start+1) l.DropCache() encode := l.Encode(duration) results[i] = encode stats[l.ObjectId].Size.Add(uint64(len(encode))) } }(i) } wg.Wait() return results } func (r *Renderer) Flush(frameInfo types.FrameInformation) (result []string) { result = r.flush(frameInfo, r.RunningBuffer) r.RunningBuffer = r.RunningBuffer[:0] return } func (r *Renderer) flush(frameInfo types.FrameInformation, buf []*line.EventLine) (result []string) { return threadedRenderer(r.Statistics, buf, frameInfo.GetFrameDuration()) } func BakeRenderedObjectsFillables(o *types.RenderedObject) *types.RenderedObject { var baked bool drawPathList := make(shapes.DrawPathList, 0, len(o.DrawPathList)) for _, command := range o.DrawPathList { if fillStyleRecord, ok := command.Style.(*shapes.FillStyleRecord); ok && !fillStyleRecord.IsFlat() { baked = true flattened := fillStyleRecord.Flatten(command.Shape) drawPathList = append(drawPathList, flattened...) } else { drawPathList = append(drawPathList, command) } } if baked { return &types.RenderedObject{ Depth: o.Depth, ObjectId: o.ObjectId, DrawPathList: drawPathList, Clip: o.Clip, ColorTransform: o.ColorTransform, MatrixTransform: o.MatrixTransform, } } else { return o } } func RenderedObjectDepthSort(a, b *types.RenderedObject) int { if len(b.Depth) > len(a.Depth) { for i, depth := range b.Depth { var otherDepth uint16 if i < len(a.Depth) { otherDepth = a.Depth[i] } if depth != otherDepth { return int(otherDepth) - int(depth) } } } else { for i, depth := range a.Depth { var otherDepth uint16 if i < len(b.Depth) { otherDepth = b.Depth[i] } if depth != otherDepth { return int(depth) - int(otherDepth) } } } return 0 }