DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

A Pixel Diff Cannot Converge, So Your Screenshot-to-Code Loop Is Following a Flat Signal

Generate markup, render it, compare the render to the target, correct, repeat. Every screenshot-to-code demo has that loop. Three things decide whether it is worth running, and none of them are the model.

Repo: https://github.com/dev48v/render-loop — PUBLIC, MIT, 25 tests, standard library only, no browser.
The whole loop runs in your browser: https://dev48.infy.uk/agentlab/vol2-01-render-loop.html

1. A pixel diff cannot converge

Two renders can be structurally identical and differ on thousands of pixels — a font advanced differently, an edge landed on a half-pixel. So the score never reaches zero.

Worse than the floor is the saturation. Once a candidate is wrong enough that its boxes no longer overlap the target's, further error changes the pixel score by almost nothing:

error structural pixel pixel step
+16 3.20 0.2946 +0.0179
+24 6.70 0.3125 +0.0179
+34 10.20 0.3214 +0.0089
+48 13.70 0.3293 +0.0079

Structural keeps climbing. Pixel has flattened to steps of 0.008 — a 441× difference in remaining signal. A loop driven by that number is taking random walks with extra steps.

The structural comparison matches boxes to boxes and reaches exactly zero when the layouts agree, which is the property that lets a loop terminate at all.

assert structural_diff(boxes, boxes).value == 0.0
total += (missing + extra) * 10.0     # a whole missing element is not a nudge
Enter fullscreen mode Exit fullscreen mode

That * 10 matters: a score that ranks "wrong size" and "absent entirely" the same will happily delete things to reduce error.

2. Best-so-far, not last

Every correction can make things worse. A loop that returns its final attempt returns whatever its last mistake produced.

report = run_loop(TARGET, scripted([good, awful, awful]), patience=5)

assert report.best_source == good
assert report.attempts[-1].score > report.best_score    # 51.13 vs 0.25
Enter fullscreen mode Exit fullscreen mode

One variable. It is the cheapest correctness property in the project, and it is missing from most implementations because on a good run the two are the same.

3. The generator never sees the answer

Easier to get wrong by accident than it sounds — one shared object is enough. The proposer receives the previous source, its rendered boxes, and a score. Never the target markup.

def test_the_generator_never_receives_the_target_source():
    run_loop(TARGET, spy, max_attempts=2)
    assert TARGET not in seen        # only the previous source
    assert seen[0] == ""             # and the first call starts from nothing
Enter fullscreen mode Exit fullscreen mode

There is a peeking() generator in the repo that is handed the answer. It exists purely as a control: it matches on the first attempt, and nothing else in the suite is allowed to. A test that the honest loop is honest means very little without one showing what cheating looks like.

Five outcomes, and it always names one

matched · no_improvement · budget · gave_up · unrenderable

An unrenderable target stops before a single candidate is generated — no signal to work against means any output is a guess. Unrenderable output is a failed attempt rather than a crash, because a generator emitting broken markup is the normal case.

And the shipped default generator cannot solve a general target, on purpose, so python -m render_loop ends in an honest no_improvement rather than a demo tuned to its own example.

Agent Lab Vol 2 opens here. Vol 1 was five tools that knew when to stop; Vol 2 starts with one that knows when its own feedback signal is lying to it: https://dev48.infy.uk/agentlab.php

Top comments (0)