Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,12 @@ derivation both the encode and preview paths must use.
- Deinterlacing enabled → `_FieldBased` comes from `pipeline.deinterlace.tff`.
- Deinterlacing disabled → falls back to `job.detected_field_order` (zimg
honours `_FieldBased` for chroma resampling too).
- **A filter that works vertically must not run on a woven interlaced frame**:
it blends the two fields on every moving edge. `ScriptGenerator::still_interlaced_tff`
(deinterlacing off + a detected order) is the one test for "still
interlaced"; Deblock uses it to run every method per field
(`{{#DEBLOCK_PER_FIELD}}`) — DCTFilter on the woven frame scored 1.5–3.7 dB
*below no filtering* (docs/reports/2026-11-deblock-order).

### Deinterlace Working Format (issue #49)

Expand Down
33 changes: 33 additions & 0 deletions app/test/integration_filter_parameters_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,39 @@ void main() {
print(' PASS');
}, timeout: const Timeout(Duration(minutes: 2)));

test('deblock: interlaced output is deblocked per field', () async {
// Deinterlacing off + a detected field order: every method runs between
// SeparateFields and DoubleWeave (docs/reports/2026-11-deblock-order).
loadSchema('deblock');
for (final method in DeblockMethod.values) {
final job = buildJob(
testName: 'deblock_per_field_${method.name}',
deblock: DeblockParameters(enabled: true, method: method),
).copyWith(detectedFieldOrder: FieldOrder.bottomFieldFirst);
final script = await generateScriptViaWorker(job);
final split = script.indexOf('core.std.SeparateFields(clip, tff=False)');
final weave = script.indexOf('core.std.DoubleWeave(clip, tff=False)[::2]');
expect(split, greaterThan(0), reason: '$method: no field split');
expect(weave, greaterThan(split), reason: '$method: no weave after split');
final call = switch (method) {
DeblockMethod.deblockQed => 'haf.Deblock_QED(',
DeblockMethod.deblock => 'core.deblock.Deblock(',
DeblockMethod.dctFilter => 'core.dctf.DCTFilter(',
};
final at = script.indexOf(call);
expect(at > split && at < weave, isTrue,
reason: '$method: the filter must run on the separated fields');
}
// Without a field order the source is treated as progressive.
final progressive = await generateScriptViaWorker(buildJob(
testName: 'deblock_whole_frame',
deblock: const DeblockParameters(enabled: true),
));
expect(progressive, contains('haf.Deblock_QED('));
expect(progressive, isNot(contains('_deblock_per_field')));
print(' PASS');
}, timeout: const Timeout(Duration(minutes: 2)));

test('grain: AddGrain params are not depth-scaled', () async {
loadSchema('grain');
final job = buildJob(
Expand Down
52 changes: 52 additions & 0 deletions app/test/integration_new_passes_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ library;
// ignore_for_file: avoid_print — these tests print diagnostics to the test log.

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
Expand Down Expand Up @@ -605,6 +606,57 @@ void main() {
await _expectValidVideo(result);
}, timeout: const Timeout(Duration(minutes: 6)));

// Deinterlacing off on an interlaced source: every Deblock method must work
// on each field, never the woven frame (docs/reports/2026-11-deblock-order).
// A job with no detected field order still takes the whole-frame path, so
// it is the in-test reference for what woven filtering does: DCTFilter on
// the woven frame attenuates the combing between fields and moves frame 45
// ~2.2 levels from the source, against ~0.6 per field.
test('deblock: interlaced output is deblocked per field, every method',
() async {
VideoJob job(String name, DeblockParameters deblock, FieldOrder? order) =>
_baseJob(
name,
pipeline: ProcessingPipeline(
deinterlace: const QTGMCParameters(enabled: false),
deblock: deblock,
),
).copyWith(
detectedFieldOrder: order,
inputWidth: 720,
inputHeight: 576,
inputPixelFormat: 'yuv422p',
inputFrameRate: 25,
totalFrames: 79,
);
Future<Uint8List> frame(VideoJob j, String label) async {
final r = await WorkerHarness.runPreview(j.toJson(), frame: 45, label: label);
expect(r.success, isTrue, reason: '$label preview failed: $r');
return WorkerHarness.imageToRgb24(r.png!, label: label);
}

for (final method in DeblockMethod.values) {
final params = DeblockParameters(enabled: true, method: method);
final name = 'deblock_per_field_${method.name}';
final result = await WorkerHarness.runJob(
job(name, params, FieldOrder.topFieldFirst).toJson(),
label: name);
await _expectValidVideo(result);
}

const off = DeblockParameters(enabled: false);
const dct = DeblockParameters(enabled: true, method: DeblockMethod.dctFilter);
final source = await frame(job('src', off, FieldOrder.topFieldFirst), 'dbf_src');
final perField = WorkerHarness.meanAbsDiff(
await frame(job('pf', dct, FieldOrder.topFieldFirst), 'dbf_field'), source);
final woven = WorkerHarness.meanAbsDiff(
await frame(job('wv', dct, null), 'dbf_woven'), source);
print(' DCTFilter vs source, frame 45: per field $perField, woven $woven');
expect(woven, greaterThan(perField * 2),
reason: 'per-field DCTFilter should change the picture far less than '
'woven-frame DCTFilter, which blends the fields');
}, timeout: const Timeout(Duration(minutes: 10)));

test('grain: AddGrain and GrainFactory3 both run end-to-end', () async {
for (final entry in <String, GrainParameters>{
'grain_add': const GrainParameters(enabled: true, var_: 6.0, uvar: 2.0),
Expand Down
32 changes: 32 additions & 0 deletions docs/ENGINEERING_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2775,6 +2775,38 @@ Recommendation, if built: an `advancedOnly` Deinterlace method with its own
small control set, tuned as above, vs-jetpack and mvutensils pinned
together. x86 speed and the v2 CPU tier are unmeasured.

### Deblock before or after deinterlacing: order kept, interlaced output deblocked per field (2026-11-03)

Full report with screenshots and scripts:
[docs/reports/2026-11-deblock-order/](reports/2026-11-deblock-order/index.html).

A user asked whether deblocking should precede deinterlacing (Gemini's
advice). It should in theory: `Deblock_QED` / `deblock.Deblock` assume an 8x8
grid from (0,0), exact only on the decoded interlaced frame, and QTGMC moves
the horizontal block edges. Measured on two 720x576 TFF fixtures (film
ground truth; a real capture against QTGMC of its lossless original), each
MPEG-2 at 1000/1500/3000 kbps with `+ildct+ilme`:

- **Deinterlacing on:** per-field deblocking before QTGMC beats today's
after-QTGMC order by ≤0.05 dB at the 24/26 default (0.05–0.14 dB at
36/44), blockiness equal. Not worth reordering.
- **Deinterlacing off:** the deblocker ran on the woven frame; per field
(`SeparateFields → deblock → DoubleWeave[::2]`) won on PSNR and SSIM in
all 14 edge-deblocker comparisons, +0.03 to +0.30 dB. **DCTFilter on the
woven frame was destructive**: 1.5–3.7 dB *below no filtering*, because it
attenuates the field-to-field alternation on moving edges as high-frequency
detail; per field it is neutral to slightly positive. Built: every method
now runs per field when `ScriptGenerator::still_interlaced_tff` says the
picture is still interlaced (`{{#DEBLOCK_PER_FIELD}}`, both templates;
`test_183`). A 4:2:0 frame with an odd chroma field height (486 lines)
can't be split and keeps the woven path. The round trip is bit-exact with
frame props restored.
- At 1000 kbps a stronger Deblock_QED (36/44) gained ~3x what 24/26 did
(+0.48 against +0.16 dB): more than any order choice. One clip, one
strength; not a reason to change the default without its own probe.
- The film fixture's reference carries `small_clip.mp4`'s own (upscaled)
H.264 blocks; PSNR rewards deblocking's smoothing, and VMAF dips slightly.

### Testing a deps change before publishing

A PR that changes `deps/` has a chicken-and-egg problem: `ci-test.yml` and
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading