String Media Object Ecosystem¶
This document provides a comprehensive overview of the parallel text rendering
pipeline in ZooUI, explaining how StringMediaObject renders text using a
hybrid dual-mode approach, viewport-aware priority batching, and thread-pool-based
layout pre-calculation to maintain sharp rendering during zoom and high quality
when stationary.
Overview¶
The string rendering ecosystem is responsible for:
Rendering text strings at any zoom level with automatic font scaling
Switching between a fast moving-mode (direct
drawTextor parallel layout) and a high-quality static mode (cachedQImageblitting)Pre-calculating text layouts in background threads to avoid blocking the render loop
Prioritizing objects closest to the viewport center via distance-based batching
Caching pre-calculated layouts and rendered images to minimize redundant work
The system spans 5 components across 5 files (~2,200 lines total) organized
as a five-layer pipeline from StringMediaObject.render() through
PriorityBatcher → ParallelLayoutCalculator → TextLayoutData
→ SceneParallelRenderer.
Architecture¶
Scene.render(painter, draft)
│
├─ [scene.vzmoving = True] ───────────────────────────────────────────┐
│ │
│ SceneParallelRenderer.precalculate_text_layouts() │
│ │ │
│ ├─ _get_text_objects() → all StringMediaObjects [RLock] │
│ │ │
│ ├─ PriorityBatcher.update_viewport() → clear batches │
│ ├─ PriorityBatcher.add_objects() → register all objects │
│ ├─ PriorityBatcher.create_batches() │
│ │ │ │
│ │ ├─ calculate_distance() → Euclidean to viewport center │
│ │ ├─ get_priority() → HIGH / MEDIUM / LOW / BACKGROUND │
│ │ └─ heapq sorted into batches of batch_size │
│ │ │
│ └─ For each batch: │
│ ParallelLayoutCalculator.submit_batch(batch, viewport, cb) │
│ │ │
│ ├─ Check results_cache [RLock] → reuse if valid │
│ └─ MISS → dispatch to WORKER THREADS: │
│ TextLayoutData.from_string_object(obj, viewport)│
│ → plain data, NO Qt objects constructed │
│ → stored under RLock, callback invoked │
│ │
│ SceneParallelRenderer.render_text(painter) │
│ │ │
│ └─ For each batch / PrioritizedObject: │
│ layout_data = get_layout_cache(index) │
│ if valid and not stale: │
│ obj.render_with_layout(painter, layout_data) │
│ → TextLayoutData.render(painter) [MAIN THREAD] │
│ → to_qfont() constructs QFont safely on main thread │
│ → drawText() per line │
│ │
└─ Main render loop: skip already-parallel-rendered text objects──────
├─ [scene.vzmoving = False] ──────────────────────────────────────────┐
│ │
│ StringMediaObject.render(painter, mode) [via main loop] │
│ │ │
│ ├─ __is_image_cache_valid(scale, mode)? │
│ │ YES → painter.drawImage(__cached_text_image) [instant] │
│ │ NO → __render_text_to_image() → cache → drawImage │
│ │ │
│ └─ Cache stores: image, scale, mode, content hash │
└─────────────────────────────────────────────────────────────────────
Dual-Mode Decision Logic¶
The entire mode decision hinges on a single property: ``Scene.vzmoving``.
Definition (from PhysicalObject):
@property
def vzmoving(self) -> bool:
if self.vz != 0:
return True
for mediaobject in self.__objects:
if mediaobject.vzmoving:
return True
return False
vzmoving is True when any zoom velocity is non-zero — the scene’s
own vz or any contained object’s vz. Velocity is damped exponentially:
when abs(vz) < 0.4 it is clamped to zero, and vzmoving becomes False.
Decision Matrix:
Condition |
Render Path |
Purpose |
|---|---|---|
|
|
Fast parallel rendering from pre-calculated layout |
|
|
Smooth zoom fallback |
|
|
Maximum quality, zero recomputation |
|
|
First frame after movement stops |
Mode Transition:
When the scene transitions from static to moving, __was_static triggers
invalidate_cache() immediately to prevent stale cached images from being
blitted during movement.
Cache State Machine¶
Image Cache (Static Mode)¶
Four state variables control the static-mode image cache:
Variable |
Type |
Purpose |
|---|---|---|
|
|
Rendered text as a premultiplied ARGB image |
|
|
Scale at which the image was rendered |
|
|
Render mode (Draft / HighQuality) used |
|
|
Combined hash of text content and color |
Cache Validation (``__is_image_cache_valid()``):
The cache is valid only if ALL of these conditions are true:
__cached_text_imageis notNoneScale mode metadata is not
NoneNeither the cached nor current scale is zero
Relative scale difference ≤ 1%:
|current - cached| / cached ≤ 0.01Mode matches:
mode == __cached_image_modeContent hash matches:
__compute_text_hash() == __cached_text_hash
Invalidation:
invalidate_cache() clears all image cache state. Triggered by:
- Movement starting (__was_static was True, vzmoving becomes True)
- Text content changed (lines setter)
- Manual call via public API
Layout Cache (Moving Mode)¶
Pre-calculated layouts are managed by PriorityBatcher and
ParallelLayoutCalculator, not by StringMediaObject directly.
Layouts are considered stale after 1000 ms (configurable via
cache_max_age_ms).
Priority Batcher¶
PriorityBatcher organizes text objects into priority batches based on
their distance from the viewport center, ensuring objects closest to the user’s
focus are rendered first.
Priority Levels:
BatchPriority.HIGH— ≤ 1000 scene units, rendered firstBatchPriority.MEDIUM— ≤ 2000 scene units, rendered secondBatchPriority.LOW— ≤ 4000 scene units, rendered thirdBatchPriority.BACKGROUND— > 4000 scene units, off-screen pre-calculation
Batching Algorithm:
1. For each StringMediaObject with text and valid position:
a. Calculate Euclidean distance to viewport center
b. Map distance to a BatchPriority via thresholds
c. Create PrioritizedObject(priority, distance, index, obj)
d. Push onto a min-heap (lower priority value = higher importance)
2. Objects outside viewport → downgraded to BACKGROUND
3. Pop from heap into batches of batch_size (default: 10)
Stop after max_batches batches (default: 10)
Skip BACKGROUND objects once ≥ half of max_batches are filled
This produces groups of objects ordered by importance, with background objects only included when there is spare capacity.
Parallel Layout Calculator¶
ParallelLayoutCalculator manages concurrent text layout computation
using a thread pool. It dispatches layout jobs to worker threads and caches
results for reuse.
Thread Pool:
Default workers:
min(32, cpu_count + 4); configurable viamax_workersBounded queue:
queue.Queue(maxsize=100)prevents memory blowoutWorkers are daemon threads — won’t block process exit
shutdown_event(threading.Event) for coordinated shutdownEach worker runs
_worker_loop()withqueue.get(timeout=0.1)polling
Cache-First Strategy:
When processing a calculation:
Check
results_cacheunderthreading.RLockIf a
COMPLETEDresult exists with non-stale layout data → reuseOn cache miss: call
TextLayoutData.from_string_object(obj, viewport)on the worker thread (extracts only plain Python data)Store result under lock, invoke callback if provided
CalculationResult:
@dataclass
class CalculationResult:
status: CalculationStatus # PENDING / IN_PROGRESS / COMPLETED / FAILED
layout_data: TextLayoutData | None
error: Exception | None
processing_time_ms: float
timestamp: float
Text Layout Data¶
TextLayoutData is a dataclass storing pre-calculated text layout
information. It is the thread-safety boundary between the worker-thread
layout calculation and the main-thread rendering.
Design Principle:
All data is stored as plain Python types — no Qt GUI objects (QFont,
QColor) are stored or constructed until render() is called on the
main thread.
Key Fields:
Field |
Type |
Purpose |
|---|---|---|
|
|
The full string content |
|
|
Font family name (e.g. |
|
|
Scaled font point size |
|
|
Integer weight constant (not a QFont object) |
|
|
RGBA components (decomposed from QColor) |
|
|
Object bounding box (value type, thread-safe) |
|
|
Per-line bounding rects for multiline text |
|
|
Milliseconds since epoch (staleness check) |
|
|
Manual invalidation flag |
Worker-Thread Factory: ``from_string_object(obj, viewport)``
Extracts layout data from a StringMediaObject without constructing
any Qt GUI objects:
Reads text via
obj._get_text()→strReads font properties via
obj._get_font()→dict(NOT QFont)Reads color components via
obj._get_color().red()/.green()/...()→int(read-only access to QColor components, safe from any thread)Reads position, width, height as plain Python values
Returns a fully populated
TextLayoutData
Main-Thread Renderer: ``render(painter)``
Constructs Qt objects on the calling thread (must be main thread):
def render(self, painter):
font = self.to_qfont() # Constructs QFont HERE
color = self.to_qcolor() # Constructs QColor HERE
painter.setFont(font)
painter.setPen(color)
for line_rect in self.line_rects:
painter.drawText(line_rect, self.text)
Layout data is considered stale after 1000 ms (configurable) and is recalculated on the next render cycle if expired.
Scene Parallel Renderer¶
SceneParallelRenderer orchestrates the entire parallel rendering
pipeline from within Scene.render().
Per-Frame Integration:
Scene.render(painter, draft)
│
├─ If vzmoving and enabled:
│ precalculate_text_layouts()
│ ├─ _get_text_objects() [RLock]
│ ├─ PriorityBatcher.add_objects() → create_batches()
│ └─ submit_batch() × N → worker threads
│
├─ If vzmoving and enabled:
│ render_text(painter)
│ ├─ For each batch, each PrioritizedObject:
│ │ layout_data → if valid → render_with_layout()
│ └─ Collect ids → skip in main loop
│
└─ Main render loop:
For each mediaobject:
if StringMediaObject and id in parallel_rendered → SKIP
else → mediaobject.render(painter, mode)
Configuration (``parallel_rendering`` section):
Key |
Type |
Default |
Purpose |
|---|---|---|---|
|
|
|
Master switch |
|
|
|
Thread pool size |
|
|
|
Objects per batch |
|
|
|
Maximum number of batches |
|
|
|
Batch calculation timeout |
|
|
|
Performance profiling output |
Lifecycle:
Init: Scene.__init__ → SceneParallelRenderer(scene, config)
Lazy-init: first precalculate_text_layouts() → initialize()
→ create calculator + batcher
Per-frame: render() → precalculate → submit batches
→ render from cached layouts
Shutdown: Scene.__del__ → shutdown()
shutdown_event.set() → poison pills → thread_pool.shutdown()
→ worker.join(timeout=1s)
Thread Safety Architecture¶
The single most important safety constraint is: no Qt GUI objects (QFont,
QFontMetrics) are constructed on worker threads. Qt’s font database and
glyph caches are not thread-safe — creating them from a non-main thread causes
C++-level races and SIGSEGV.
Safety Layers:
Layer |
Mechanism |
What It Protects |
|---|---|---|
Scene object access |
|
All reads/writes to the object list |
Font construction guard |
|
Returns cached font; defers QFont construction to main thread |
|
Plain Python types only |
Can be created on any thread without Qt interaction |
|
Font properties as |
No QFont created on worker threads |
|
Lazy construction on calling thread |
QFont built only in |
Results cache |
|
Safe concurrent read/write |
Calculation queue |
|
Work distribution to workers |
Shutdown |
|
Coordinated graceful shutdown |
Performance Characteristics¶
Cost by Mode:
Mode |
Cost per frame (per object) |
Thread |
|---|---|---|
Cached image (static) |
|
Main thread only |
Parallel layout (moving) |
|
Layout on workers; render on main |
Direct drawText (fallback) |
Full |
Main thread only (blocking) |
Optimization Techniques:
Skip already-rendered: Main loop skips parallel-rendered text objects via
id()-basedsetmembership — O(1) lookupType check optimization:
type(obj).__name__ == "StringMediaObject"instead ofisinstance()— ~2.8× fasterFont/metrics caching:
__fontproperty returns cached QFont when scale hasn’t changedPremultiplied alpha:
Format_ARGB8565_Premultipliedfor cached images — fastest composition on Qt’s raster paint engineViewport change gating: Batcher only updates when viewport has moved by >10 scene units
``math.exp2()``: Used instead of
2**xfor zoom exponentiation (1.85× faster)
Resource Cleanup:
Worker threads are daemon — won’t block process exit
Scene.__del__()andshutdown_threads()guarantee clean terminationQueue poison pills (
Nonesentinel) for each workerworker.join(timeout=1.0)prevents indefinite hangs
Configuration Tuning¶
Per-object Control:
# Disable parallel rendering for a specific object
string_obj.enable_parallel_rendering(False)
# Clear all caches
string_obj.clear_caches()
Render Loop Integration:
# Check if parallel rendering is active
if scene.parallel_renderer.is_enabled():
stats = scene.parallel_renderer.get_stats()
print(f"Cache hit rate: {stats['cache_hit_rate']:.1%}")
print(f"Batches processed: {stats['batches_processed']}")
Recommended Settings:
Scenario |
|
|
|
|---|---|---|---|
High-density text (1000+ objects) |
8–16 |
20 |
20 |
Low-latency interaction |
2–4 |
5 |
10 |
Memory-constrained |
2 |
10 |
5 |
Debug/development |
1 |
5 |
3 |
Usage Examples¶
Checking Parallel Rendering Status¶
scene = window.current_zui.scene
renderer = scene.parallel_renderer
if renderer.is_enabled():
stats = renderer.get_stats()
print(f"Text objects: {stats['total_text_objects']}")
print(f"Batches: {stats['batches_processed']}")
print(f"Cache hit rate: {stats['cache_hit_rate']:.1%}")
Disabling Parallel Rendering¶
# Disable globally via config
config = {"parallel_rendering": {"enabled": False}}
scene = Scene(config=config)
# Disable at runtime
scene.parallel_renderer.enabled = False
# Disable per-object
string_obj.enable_parallel_rendering(False)
Clearing Caches¶
# Clear all StringMediaObject caches
for obj in scene.objects:
if type(obj).__name__ == "StringMediaObject":
obj.clear_caches()
# Force recalculation of all layouts
scene.parallel_renderer.invalidate_all_caches()
Synchronous Layout Calculation (Debugging)¶
from zooui.objects.mediaobjects.mediaobjectsutils.string.parallellayout import (
ParallelLayoutCalculator,
)
calculator = ParallelLayoutCalculator(max_workers=1)
# Calculate layout synchronously (runs on calling thread)
result = calculator.calculate_layout_sync(string_obj, viewport_rect)
if result.layout_data:
result.layout_data.render(painter)
See Also¶
Object System — Object system architecture with StringMediaObject details
Configuration System — Configuration system (parallel_rendering section)
Window System — Window system rendering integration
../zooui/prioritybatcher — PriorityBatcher API reference
../zooui/parallellayout — ParallelLayoutCalculator API reference
../zooui/textlayout — TextLayoutData API reference
../zooui/stringmediaobject — StringMediaObject API reference