Skip to content

Cache drawImage's loaded and scaled surfaces by file path - #27

Merged
dmccoystephenson merged 1 commit into
mainfrom
feature/cache-draw-image
Jul 30, 2026
Merged

Cache drawImage's loaded and scaled surfaces by file path#27
dmccoystephenson merged 1 commit into
mainfrom
feature/cache-draw-image

Conversation

@dmccoystephenson

@dmccoystephenson dmccoystephenson commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

  • drawImage previously called pygame.image.load and pygame.transform.scale on every single call, re-reading and re-decoding the same asset file from disk every frame (measured at ~1.04 ms/call, dominated by the ~0.57 ms decode).
  • Caches the loaded (unscaled) surface by filePath, and separately caches the most recent (size, scaled surface) pair per filePath — so a steady-state render loop (same size every frame) pays the load/scale cost exactly once, while an animated zoom cannot grow the cache unbounded.
  • Mirrors the existing _getFont cache's rationale, but does not need its display-session invalidation logic: an unconverted pygame.image.load surface survives a pygame.quit()/init() cycle (verified empirically), unlike a Font built against SDL_ttf.
  • A failed load still caches nothing, so a missing path keeps raising on every call (test_draw_image_missing_file_raises still passes unmodified).
  • No public signature change — purely internal caching, so this is additive/behavior-preserving for all downstream consumers (Roam/Apex/Ophidian/Patchwork/Tic-Tak-Toe never call drawImage today per a quick search, but the signature is unchanged regardless).
  • Documented the cache's disk-edit-mid-run tradeoff both in the method's comment and in the README's drawImage entry, per the issue's suggestion to document rather than work around it.

Test plan

  • python3 -m py_compile src/main/python/preponderous/graphik/graphik.py
  • python3 -m pytest — 24 passed (20 pre-existing + 4 new)
  • New tests empirically confirmed to fail without the fix (stash-and-run): test_draw_image_reuses_loaded_surface_across_calls and test_draw_image_reuses_scaled_surface_for_the_same_size both failed (5 loads/scales instead of 1) with graphik.py reverted, and passed once restored.
  • test_draw_image_rescales_when_size_changes confirms the per-path cache holds only the most recent size, not every size ever requested.
  • test_draw_image_still_blits_correctly_after_caching confirms cached surfaces still blit correct pixels.

Closes #23


This PR description was drafted during a Gardener session (Stephenson-Software/gardener).

drawImage re-read and re-decoded the source file, and rescaled it, on
every call -- a 60fps render loop redid both 60 times a second even
though the bytes on disk never changed. Cache the loaded surface plus
the most recent (size, scaled surface) pair per filePath, mirroring
the existing _getFont cache. Unlike fonts, an unconverted surface
survives a pygame.quit()/init() cycle, so no session-invalidation
logic is needed here.

Closes #23

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dmccoystephenson

Copy link
Copy Markdown
Member Author

Self-review rubric:

Universal

  • Scope: PASS — only graphik.py, its test file, and one README line (the drawImage doc entry) were touched; all necessary for issue drawImage re-reads and re-scales the source file on every call #23.
  • Tests-new: PASS — every new caching behavior has a dedicated test (test_draw_image_reuses_loaded_surface_across_calls, test_draw_image_reuses_scaled_surface_for_the_same_size, test_draw_image_rescales_when_size_changes, test_draw_image_still_blits_correctly_after_caching).
  • Tests-fix (empirical): PASS — stashed graphik.py alone, reran the new tests: test_draw_image_reuses_loaded_surface_across_calls and test_draw_image_reuses_scaled_surface_for_the_same_size both FAILED (5 loads/scales instead of 1). Restored the fix (git stash pop) and reran: all 24 tests PASS.
  • Sibling structure: PASS — new tests follow the existing file's helper-function + test_* pattern (mirrors _count_font_constructions/test_draw_text_reuses_one_font_...).
  • Sibling renames: N/A — no identifier renamed.
  • Docs: PASS — README drawImage entry now documents the caching/disk-edit tradeoff; the method itself carries an explanatory comment. version.txt/pom.xml don't exist in this tree (version lives solely in _version.py, unchanged and not implicated by this change).
  • Issue resolution: PASS — issue drawImage re-reads and re-scales the source file on every call #23's exact code (drawImage, graphik.py:96-99) is rewritten to cache by filePath, per the issue's suggested design (cache loaded surface + most-recent scaled surface per path, no font-style session invalidation, missing-file test still passes unmodified).
  • Manual validation: PASS — py_compile succeeds, import smoke test succeeds, pytest exits 0 with 24 passed on the PR head (re-confirmed just now).

Repo-specific

  • camelCase: N/A — no new public method; drawImage's existing camelCase name is unchanged.
  • Backward-compat: PASS — no signature change to any public member (drawImage(filePath, xpos, ypos, width, height) is identical). This is a pure internal-caching change, so it's safe regardless of consumer call sites. (Note: gh search code ... org:Preponderous-Software was blocked by the sandbox's command-approval gate in this headless run, so I could not directly confirm which consumers call drawImage — flagging this for visibility, but it doesn't change the PASS since the public surface is unchanged either way.)
  • Headless: PASS — all new tests run under the repo's conftest.py-forced SDL_VIDEODRIVER=dummy/SDL_AUDIODRIVER=dummy, no real display/audio needed.
  • Version sync: N/A — no version string touched.
  • No new deps: PASS — no import added beyond pygame (already a dependency).

One judgment note (not a FAIL, just flagging): the cache is intentionally unbounded in the number of distinct filePaths (matches _getFont's per-size cache, which is similarly unbounded by number of sizes). For a consumer that generates many one-off image paths at runtime this could grow without limit, but that's the same tradeoff the existing font cache already accepts, and the issue didn't ask for eviction — noting it here rather than scope-creeping an LRU into this PR.

@dmccoystephenson
dmccoystephenson merged commit fab0f42 into main Jul 30, 2026
5 checks passed
@dmccoystephenson
dmccoystephenson deleted the feature/cache-draw-image branch July 30, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

drawImage re-reads and re-scales the source file on every call

1 participant