Skip to content

Overall render progress omits the post-process stage, freezing the rate and the time estimate #23

Closed

Description

@sensitron

Summary

A render runs three per-frame stages — Segmentation, Post-process, Encode — but
the overall progress budget is hard-coded to two passes:

# src/matteloop/jobs/render.py:1069
overall=(index, len(timestamps) * 2),
# src/matteloop/jobs/render.py:1094
overall_completed=index + 1, overall_total=len(timestamps) * 2,

Segmentation fills 0 → N, Encode resumes at N and runs to 2N, and Post-process
reports nothing. The overall counter therefore stands still for the entire
Post-process stage, and because the dialog derives the frame rate and the
remaining time from that counter, both freeze with it.

Observed

A 60 s clip at the default 15 fps output, so N = 900 and the dialog shows a
total of 1800. Same run, three moments:

elapsed stage stage frame overall shown rate shown remaining
23:56 Post-process 425 / 900 900 / 1800 0.6 fps 23:47
25:08 Post-process 807 / 900 900 / 1800 0.6 fps 23:47
26:04 Encode 15 / 900 915 / 1800 1.0 fps 14:39

Between the first two rows, 382 frames completed in 72 s — about 5.3 frames/s —
while the dialog reported 0.6 fps and an unchanged 23:47. The remaining time was
identical to the second, 72 seconds apart: it is not merely inaccurate, it stops
being recomputed. Actual time left at that point was roughly 18 seconds against
24 minutes displayed, a factor of about 80.

The third row shows the counter resuming in Encode, and the rate becoming
plausible again — the display is correct whenever the counter moves, and blind
exactly while Post-process runs.

Why it matters beyond cosmetics

  • The bar reads half done when two thirds of the work remains.
  • For one whole stage the estimate stands still at a value far above the truth.
    A user watching "23:47 remaining" not move for minutes has good reason to
    press Cancel on a render that is seconds from finishing.

The whole run took roughly 25.5 minutes; the dialog spent about six of those
claiming twenty-four were left.

Environment

Windows 11 26200, main at 22b77be plus #20's branch for the DirectML
runtime, BiRefNet Portrait on DmlExecutionProvider (Radeon RX 7900 XT).
The stage timings differ with hardware; the counter arithmetic does not.

Note

len(timestamps) * 2 appears twice and is the whole defect, but the fix is a
design choice rather than a constant: the three stages run at very different
rates here (roughly 0.7, 5.3 and ~1 frames/s), so counting them as equal units
would still produce a poor estimate even once Post-process is included.
Weighting them, or estimating per stage, would report honestly.

Activity

  1. self-assigned this
    on Sep 4, 2026
  2. smb commented on Sep 4, 2026

    @smb
    Collaborator

    Taking this one; branch fix/overall-progress-stages.

    Your closing note is right that the fix is a choice rather than a constant, and #27 landing since you filed this makes the choice narrower than it looked.

    The rebuild path has the same defect, worse. _encode_snapshot is now reached from two callers and already carries a rebuilt flag. rebuild() sets no overall context at all, so the counter is empty through the whole post-process stage and the encode stage then starts a fresh 0..frame_count scale — the bar jumps from nothing to a new maximum mid-job. Since the transform stage exists to make the cheap re-encode the normal path, that is now the progress users will see most often.

    Trimming makes the stages different lengths. The per-frame post-process loop moved to transform_stage.stage_encoder_frames, which stages only transform.kept_range(frame_count). After a trim, segmentation runs over the full timestamp grid while post-process and encode run over the kept frames only. So no single multiplier on one frame count is correct, independently of whether it is two or three.

    The exact freeze is one line short of where the constant is: that loop calls context.progress("Framing", …) with no overall_completed/overall_total, so it publishes stage progress while leaving the overall figure untouched.

    What I am implementing:

    Case Budget
    Full render timestamps + 2 × kept — segmentation, then post-process, then encode
    Transform rebuild 2 × kept — post-process, then encode
    Auto-fit (max_bytes) encode stays indeterminate exactly as today; the earlier stages report their real share

    Deliberately not doing the weighted or per-stage-rate estimate you raised. The measured defect is a counter that stops and an estimate that stops being recomputed, and honest unit counting fixes both; the stages having different rates makes the estimate imprecise, which is a smaller and separate problem. If it still reads badly on a real run with the rates you measured (0.7 / 5.3 / ~1 fps), that is worth its own issue with the numbers from the fixed build.

    Tests cover all four shapes including a trimmed render.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions