Skip to content

feat: add image-pixels-testcard example (flat RGBA pixel buffer + save() lifecycle witness)#60

Merged
TMHSDigital merged 1 commit into
mainfrom
feat/image-pixels-testcard
Jul 18, 2026
Merged

feat: add image-pixels-testcard example (flat RGBA pixel buffer + save() lifecycle witness)#60
TMHSDigital merged 1 commit into
mainfrom
feat/image-pixels-testcard

Conversation

@TMHSDigital

Copy link
Copy Markdown
Owner

Summary

Adds examples/image-pixels-testcard — a procedural broadcast test card (seven neon bars, luminance ramp, PLUGE row with a bottom-left origin marker, the classic circle) written into bpy.data.images.new() with one pixels.foreach_set() call (512×288×4 = 589,824 floats), rendered on an emissive studio monitor. Fills the bpy.types.Image gap in the gallery; bulk pixels.foreach_set was an explicit ROADMAP candidate.

API contract witnessed

  • Image.pixels is a flat, row-major, bottom-left-origin float buffer that is always RGBA: channels == 4 and len(pixels) == w*h*4 even with alpha=False; an RGB-stride foreach_set raises TypeError.
  • Byte images store 8 bits/channel: round-trip error ≤ 0.5/255 and strictly > 0; float_buffer=True round-trips at float32 precision.
  • Image.scale() reallocates the buffer; a stale-size foreach_get raises instead of shearing rows.
  • The save() trap (discovered while authoring, identical on 4.5 LTS and 5.1): save() on a GENERATED image silently flips source to FILE and drops the in-memory buffer (has_data → False); every later pixels read re-loads from whatever sits at filepath_raw. save_render() writes the same PNG non-destructively.

What each check catches (falsified once, then restored)

Check Failure it catches Falsification proof
RGB-stride write raises code assuming 3-channel buffers W*H*3 write → exit 3
Round-trip vs closed-form card stride/orientation/packing bugs one-pixel shift → exit 4, measured err 0.9700000 vs tol 0.0019618
byte_err > 0 byte/float storage swapping (structural — an exact byte round-trip fails)
scale() reallocation stale-size reads succeeding (structural)
save() source-flip + imposter read the buffer-drop lifecycle changing substituting save_render() → exit 7
byte PNG save/reload disk round-trip drift (tolerance-gated)

Version divergence

None — every contract asserts identically on 4.5.11 LTS and 5.1.2 (probed both before authoring). Only the optional render path gates the EEVEE engine id.

Visual notes

Dark-studio monitor at three-quarter angle, emissive card (Closest interpolation so the jagged circle edge is the actual 512×288 buffer), teal underglow accent, warm rim. The white PLUGE square sits bottom-left because that is where pixel (0,0) lives — a top-down origin assumption would visibly flip it. Iterated four times (blank screen from missing UVs → washed-out bars → clipped circle/stray glow ends → final).

Test plan

Proven by live run on this machine:

  • Check-only exits 0 on Blender 5.1.2 and 4.5.11 LTS (portable builds), printing measured values: byte err 0.0019608 (tol 0.0019618), float 2.97e-08, post-save imposter dev 0.0000000, byte PNG reload 0.0019608
  • Two falsification runs exit non-zero (4 and 7) with correct diagnostics
  • Full render on 5.1 EEVEE, inspected at full size and thumbnail size
  • python scripts/build_gallery.py regenerated docs/gallery/ (20 detail pages)

By inspection / CI only:

  • Linux xvfb-run smoke steps (this PR's 4.5/5.1 jobs)
  • --engine cycles render path (code-identical to sibling examples; not exercised locally)

🤖 Generated with Claude Code

…e() lifecycle witness)

The gallery had no coverage of bpy.types.Image, and bulk pixels.foreach_set
was an explicit ROADMAP candidate. A procedural broadcast test card written
with one foreach_set witnesses the contracts AI-generated image code gets
wrong: the buffer is always flat RGBA (channels == 4 even with alpha=False),
byte storage quantizes at exactly <= 0.5/255 and strictly > 0, scale()
reallocates so stale-size bulk reads raise, and Image.save() silently flips
source to FILE and drops the in-memory buffer -- later pixels reads come from
whatever sits on disk, proven by overwriting the file with an imposter.
save_render() is the non-destructive path. All contracts probed identical on
4.5.11 LTS and 5.1.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation ci labels Jul 18, 2026
@TMHSDigital
TMHSDigital merged commit 256f98d into main Jul 18, 2026
9 checks passed
@TMHSDigital
TMHSDigital deleted the feat/image-pixels-testcard branch July 18, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant