You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4aeb805
Browse filesBrowse the repository at this point in the historyBrowse files
fix(examples): handle N jobs and job errors in thread-queue-timer; make three checks falsifiable (#478)
The thread-queue-timer pattern stopped its drain after the first result,
leaving a second job's result stranded, and a raising job left the timer
polling forever. The snippet and skill now count pending jobs, drain
everything per tick, and have the worker put the exception.
Three examples carried checks that passed by construction: the
image-pixels orientation check compared two Python lists, the extension
--validate-only falsifier reached exit 4 by tautology, and the timers
main-thread and still-registered checks could never fire. Each now
fails only through the Blender behavior it claims, with a catalog
falsifier per check.
Closes#463Closes#470
Signed-off-by: TMHSDigital <tmhospitalitystrategies@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
<p><strong>What it witnesses:</strong> the <code>bpy.types.Image</code> pixel-buffer contract. <code>Image.pixels</code> is a flat, row-major, bottom-left-origin float buffer that is <em>always</em> RGBA: <code>channels == 4</code> and <code>len(pixels) == width × height × 4</code> even when the image is created with <code>alpha=False</code>, so an RGB-stride <code>foreach_set</code> raises <code>TypeError</code> instead of writing. A byte image (the default) stores 8 bits per channel — every written float round-trips with error ≤ 0.5/255 <strong>and strictly > 0</strong> (an exact round-trip would mean storage is not 8-bit); <code>float_buffer=True</code> stores float32 and round-trips at ~1e-7. <code>Image.scale()</code> reallocates the buffer, so a <code>foreach_get</code> into a stale-size list raises <code>TypeError</code> rather than silently shearing rows.</p>
66
66
<p><strong>The <code>save()</code> trap</strong> (found while authoring — identical on 4.5 LTS and 5.1): <code>Image.save()</code> on a <code>GENERATED</code> image silently flips <code>source</code> to <code>'FILE'</code> and drops the in-memory buffer (<code>has_data</code> becomes <code>False</code>). Every later <code>pixels</code> read re-loads from whatever currently sits at <code>filepath_raw</code>. The check proves it by overwriting the file with a flat-gray imposter image <em>after</em><code>save()</code> and reading the imposter's pixels back through the original datablock. <code>save_render()</code> writes the same PNG but is non-destructive: <code>source</code> stays <code>'GENERATED'</code> and the buffer stays exact. Scripts that write pixels, <code>save()</code>, then keep computing on <code>pixels</code> are silently computing on a decoded PNG.</p>
67
67
<p><strong>What each check catches on failure:</strong></p>
68
-
<ul><li><em>Buffer geometry</em> — an API change to per-image channel counts, or code assuming an RGB stride (falsified: a <code>W*H*3</code> write raises and the check exits 3).</li><li><em>Byte/float round-trip vs the closed-form card</em> — any stride, orientation, or packing bug in the bulk path; a one-pixel shift was deliberately introduced once and the check exited 4 with measured error 0.97 against tolerance 0.00196.</li><li><em>Quantization floor</em> — <code>byte_err > 0</code> proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.</li><li><em>Reallocation</em> — <code>scale()</code> no longer reallocating (stale-size read succeeding).</li><li><em>save() lifecycle</em> — the source-flip/buffer-drop behavior changing (falsified: substituting <code>save_render()</code> for <code>save()</code> exits 7 because <code>source</code> stays <code>GENERATED</code>).</li><li><em>Disk round-trip</em> — a byte sRGB image saved to PNG and reloaded must match at quantization tolerance.</li></ul>
68
+
<ul><li><em>Buffer geometry</em> — an API change to per-image channel counts, or code assuming an RGB stride (falsified: a <code>W*H*3</code> write raises and the check exits 3).</li><li><em>Byte/float round-trip</em> — a stride or packing bug in the bulk path; a one-pixel shift was deliberately introduced once and the check exited 4 with measured error 0.97 against tolerance 0.00196. A <code>foreach_set</code> → <code>foreach_get</code> round trip reads back the order it wrote, so it cannot see row order; that is the next check's job.</li><li><em>Row order through a real PNG</em> — the byte image is saved with <code>Image.save()</code> and the file's rows are decoded with <code>zlib</code> + <code>struct</code> alone, no <code>bpy</code>. PNG stores the top row first, so a bottom-left origin puts pixel (0, 0) — the origin marker — in the file's <strong>last</strong> row, and every PNG row <code>k</code> must match card row <code>H-1-k</code> at quantization tolerance. Measured: the marker sits in PNG rows 247..287 of 0..287 and the rows match at 0.0019608 (tolerance 0.0019618). <code>--wrong-origin</code> writes the card top-down through Blender: the marker lands in PNG rows 0..40, the row error is 0.9216, and the check exits 13.</li><li><em>Quantization floor</em> — <code>byte_err > 0</code> proves 8-bit storage really quantizes; byte and float images swapping behavior cannot hide.</li><li><em>Reallocation</em> — <code>scale()</code> no longer reallocating (stale-size read succeeding).</li><li><em>save() lifecycle</em> — the source-flip/buffer-drop behavior changing (falsified: substituting <code>save_render()</code> for <code>save()</code> exits 7 because <code>source</code> stays <code>GENERATED</code>).</li><li><em>Disk round-trip</em> — a byte sRGB image saved to PNG and reloaded must match at quantization tolerance.</li></ul>
69
69
<p><strong>Version divergence:</strong> none — every contract above, including the <code>save()</code> trap, was probed and asserts identically on Blender 4.5.11 LTS and 5.1.2. The only gate in the file is the EEVEE engine id for the optional render (<code>BLENDER_EEVEE_NEXT</code> on 4.x, <code>BLENDER_EEVEE</code> on 5.x).</p>
70
70
<p><strong>Render hazard worth knowing:</strong> a bmesh-built plane has no UV map, and <code>bmesh.ops.create_grid(..., calc_uvs=True)</code> silently creates none unless a UV layer already exists — without one, an Image Texture samples texel (0,0) for every fragment and the screen renders as one flat color. The render path creates the layer explicitly.</p>
71
71
<h2id="run">Run<aclass="anchor" href="#run" aria-label="Link to this section">#</a></h2>
72
72
<pretabindex="0"><code># Cheap correctness check (no render) — the CI check:
<h2id="exit-codes">Exit codes<aclass="anchor" href="#exit-codes" aria-label="Link to this section">#</a></h2>
82
82
<p>Per-script sequential checks. <code>9</code> is a valid check code; there is no rule against it.</p>
83
-
<divclass="table-wrap"><table><thead><tr><thscope="col">Code</th><thscope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Pixel buffer is not always RGBA</td></tr><tr><td>4</td><td>Byte round-trip vs closed-form card (<code>--wrong-origin</code> lands here)</td></tr><tr><td>5</td><td>Float-buffer round-trip failed</td></tr><tr><td>6</td><td><code>scale()</code> did not reallocate, or stale-size read succeeded</td></tr><tr><td>7</td><td><code>save()</code> source/buffer-drop contract drifted</td></tr><tr><td>8</td><td><code>save_render()</code> flipped source or disturbed the buffer</td></tr><tr><td>9</td><td>Byte PNG save/reload error</td></tr><tr><td>10</td><td>Framing gate violation on the <code>--output</code> path (<code>gallery_framing</code>)</td></tr><tr><td>12</td><td><code>--output</code> produced no file</td></tr></tbody></table></div>
84
-
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code>. Its catalog falsifier is <code>--wrong-origin</code> (expects exit 4).</p>
83
+
<divclass="table-wrap"><table><thead><tr><thscope="col">Code</th><thscope="col">Meaning</th></tr></thead><tbody><tr><td>0</td><td>Success</td></tr><tr><td>1</td><td>Uncaught exception (FATAL wrapper)</td></tr><tr><td>2</td><td>argparse / usage</td></tr><tr><td>3</td><td>Pixel buffer is not always RGBA</td></tr><tr><td>4</td><td>Byte round-trip error or no quantization</td></tr><tr><td>5</td><td>Float-buffer round-trip failed</td></tr><tr><td>6</td><td><code>scale()</code> did not reallocate, or stale-size read succeeded</td></tr><tr><td>7</td><td><code>save()</code> source/buffer-drop contract drifted</td></tr><tr><td>8</td><td><code>save_render()</code> flipped source or disturbed the buffer</td></tr><tr><td>9</td><td>Byte PNG save/reload error</td></tr><tr><td>10</td><td>Framing gate violation on the <code>--output</code> path (<code>gallery_framing</code>)</td></tr><tr><td>12</td><td><code>--output</code> produced no file</td></tr><tr><td>13</td><td>Saved PNG rows do not put pixel (0, 0) at the bottom-left (<code>--wrong-origin</code> lands here)</td></tr></tbody></table></div>
84
+
<p>The <code>blender-smoke</code> workflow runs the check on Blender 5.2 LTS and 4.5 LTS (5.1 on the weekly cron, the <code>needs-5.1</code> PR label, or manual dispatch). Smoke does not pass <code>--output</code>. Its catalog falsifier is <code>--wrong-origin</code> (expects exit 13).</p>
85
85
<p>In the render, <code>Closest</code> interpolation keeps the pixel grid honest — the jagged circle edge is the 512 × 288 buffer itself, and the white marker in the PLUGE row sits at the bottom-left because that is where pixel (0, 0) lives. The monitor is staged as a broadcast reference display — slim beveled bezel over a stepped rear housing, a matte sun hood framing the picture, input keys and a teal power LED on the bottom bezel, a machined stand — on a walnut desk with drawer pedestals, on the dark studio stage from <code>docs/VISUAL-STYLE.md</code> (Standard view transform; warm key, cool fill and rim; a warm pool raking the back wall). The screen stays emissive and matte — specular off, emission strength 1.0 — so the card's values read exactly. The render path gates framing through <code>examples/gallery_framing.py</code> (exit 10) before writing; the desk is passed as stage, like the floor, because its crop at the frame bottom is the composition.</p>
0 commit comments