1package graphics
2
3import (
4 "fmt"
5 "math/bits"
6 "sort"
7 "sync"
8)
9
10type assetRecord struct {
11 id AssetID
12 encoded []byte
13 format AssetFormat
14 width int64
15 height int64
16 pixels uint64
17}
18
19type placementRecord struct {
20 id PlacementID
21 asset AssetID
22 source PixelRect
23 destination PixelRect
24 cells CellRect
25 layer int64
26 order uint64
27}
28
29type sceneState struct {
30 generation uint64
31 nextAsset uint64
32 nextPlace uint64
33 nextOrder uint64
34 assets map[AssetID]assetRecord
35 placements map[PlacementID]placementRecord
36 usage Usage
37}
38
39// Scene owns a bounded set of immutable assets and sparse placements. Scene
40// methods are safe for concurrent callers; snapshots never share mutable maps
41// with the scene. The scene itself contains no text or renderer state.
42type Scene struct {
43 mu sync.RWMutex
44 limits Limits
45 state *sceneState
46}
47
48// NewScene creates an empty bounded graphics scene. Zero-valued limits use
49// package defaults. Limits are copied and may be safely reused by the caller.
50func NewScene(limits Limits) *Scene {
51 return &Scene{limits: normalizeLimits(limits), state: emptyState()}
52}
53
54// New is a concise constructor alias.
55func New(limits Limits) *Scene { return NewScene(limits) }
56
57// NewSceneWithLimits is a descriptive constructor alias.
58func NewSceneWithLimits(limits Limits) *Scene { return NewScene(limits) }
59
60func emptyState() *sceneState {
61 return &sceneState{
62 nextAsset: 1,
63 nextPlace: 1,
64 nextOrder: 1,
65 assets: make(map[AssetID]assetRecord),
66 placements: make(map[PlacementID]placementRecord),
67 }
68}
69
70func normalizeLimits(limits Limits) Limits {
71 if limits.MaxAssets == 0 {
72 limits.MaxAssets = DefaultMaxAssets
73 }
74 if limits.MaxPlacements == 0 {
75 limits.MaxPlacements = DefaultMaxPlacements
76 }
77 if limits.MaxEncodedBytes == 0 {
78 limits.MaxEncodedBytes = DefaultMaxEncodedBytes
79 }
80 if limits.MaxDecodedPixels == 0 {
81 limits.MaxDecodedPixels = DefaultMaxDecodedPixels
82 }
83 if limits.MaxEncodedBytesPerAsset == 0 {
84 limits.MaxEncodedBytesPerAsset = DefaultMaxEncodedBytesPerAsset
85 }
86 if limits.MaxDecodedPixelsPerAsset == 0 {
87 limits.MaxDecodedPixelsPerAsset = DefaultMaxDecodedPixelsPerAsset
88 }
89 return limits
90}
91
92// Limits returns the immutable limits configured for the scene.
93func (s *Scene) Limits() Limits {
94 if s == nil {
95 return Limits{}
96 }
97 s.mu.RLock()
98 defer s.mu.RUnlock()
99 return s.limits
100}
101
102// AddAsset copies and registers an encoded asset, returning an opaque ID.
103func (s *Scene) AddAsset(blob AssetBlob) (AssetID, error) {
104 return s.addAsset(blob, true)
105}
106
107// AddAssetOwned is AddAsset for callers that hand over blob.Encoded: the scene
108// keeps the slice without copying, so the caller must not read or modify it
109// afterwards. Snapshots and AssetView still return copies. On error the scene
110// does not retain blob.Encoded, so the caller keeps ownership of it.
111func (s *Scene) AddAssetOwned(blob AssetBlob) (AssetID, error) {
112 return s.addAsset(blob, false)
113}
114
115func (s *Scene) addAsset(blob AssetBlob, copyEncoded bool) (AssetID, error) {
116 if s == nil {
117 return AssetID{}, fmt.Errorf("add asset: %w", ErrInvalidAsset)
118 }
119 s.mu.Lock()
120 defer s.mu.Unlock()
121 candidate := cloneState(s.state)
122 record, err := s.prepareAsset(blob, candidate, copyEncoded)
123 if err != nil {
124 return AssetID{}, err
125 }
126 id, ok := takeAssetID(candidate)
127 if !ok {
128 return AssetID{}, fmt.Errorf("add asset: %w", ErrIdentifierOverflow)
129 }
130 record.id = id
131 candidate.assets[id] = record
132 candidate.usage.Assets++
133 candidate.usage.EncodedBytes += uint64(len(record.encoded))
134 candidate.usage.DecodedPixels += record.pixels
135 if err := advanceGeneration(candidate); err != nil {
136 return AssetID{}, err
137 }
138 s.state = candidate
139 return id, nil
140}
141
142// RegisterAsset is an alias for AddAsset.
143func (s *Scene) RegisterAsset(blob AssetBlob) (AssetID, error) { return s.AddAsset(blob) }
144
145// ReplaceAsset atomically replaces an asset while removing all placements that
146// reference it. If validation or resource limits reject the new asset, the
147// existing asset and placements remain unchanged.
148func (s *Scene) ReplaceAsset(id AssetID, blob AssetBlob) (AssetID, error) {
149 return s.replaceAsset(id, blob, true)
150}
151
152// ReplaceAssetOwned is ReplaceAsset for callers that hand over blob.Encoded;
153// see AddAssetOwned. On error blob.Encoded is not retained.
154func (s *Scene) ReplaceAssetOwned(id AssetID, blob AssetBlob) (AssetID, error) {
155 return s.replaceAsset(id, blob, false)
156}
157
158func (s *Scene) replaceAsset(id AssetID, blob AssetBlob, copyEncoded bool) (AssetID, error) {
159 if s == nil {
160 return AssetID{}, fmt.Errorf("replace asset: %w", ErrAssetNotFound)
161 }
162 s.mu.Lock()
163 defer s.mu.Unlock()
164 candidate := cloneState(s.state)
165 old, ok := candidate.assets[id]
166 if !ok {
167 return AssetID{}, fmt.Errorf("replace asset: %w", ErrAssetNotFound)
168 }
169 for placementID, placement := range candidate.placements {
170 if placement.asset != id {
171 continue
172 }
173 delete(candidate.placements, placementID)
174 candidate.usage.Placements--
175 }
176 delete(candidate.assets, id)
177 candidate.usage.Assets--
178 candidate.usage.EncodedBytes -= uint64(len(old.encoded))
179 candidate.usage.DecodedPixels -= old.pixels
180 replacement, err := s.prepareAsset(blob, candidate, copyEncoded)
181 if err != nil {
182 return AssetID{}, err
183 }
184 replacementID, ok := takeAssetID(candidate)
185 if !ok {
186 return AssetID{}, fmt.Errorf("replace asset: %w", ErrIdentifierOverflow)
187 }
188 replacement.id = replacementID
189 candidate.assets[replacementID] = replacement
190 candidate.usage.Assets++
191 candidate.usage.EncodedBytes += uint64(len(replacement.encoded))
192 candidate.usage.DecodedPixels += replacement.pixels
193 if err := advanceGeneration(candidate); err != nil {
194 return AssetID{}, fmt.Errorf("replace asset: %w", err)
195 }
196 s.state = candidate
197 return replacementID, nil
198}
199
200// AddEncodedAsset registers an asset from its encoded bytes and decoded pixel
201// dimensions. The input bytes are copied before this method returns.
202func (s *Scene) AddEncodedAsset(encoded []byte, width, height int64) (AssetID, error) {
203 return s.AddAsset(AssetBlob{Encoded: encoded, Width: width, Height: height})
204}
205
206// RemoveAsset removes an unreferenced asset. Use RemoveAssetCascade when the
207// asset's placements should be removed as one transaction.
208func (s *Scene) RemoveAsset(id AssetID) error {
209 if s == nil {
210 return fmt.Errorf("remove asset: %w", ErrAssetNotFound)
211 }
212 s.mu.Lock()
213 defer s.mu.Unlock()
214 candidate := cloneState(s.state)
215 if err := removeAsset(candidate, id, false); err != nil {
216 return err
217 }
218 if err := advanceGeneration(candidate); err != nil {
219 return fmt.Errorf("remove asset: %w", err)
220 }
221 s.state = candidate
222 return nil
223}
224
225// RemoveAssetCascade removes an asset and all placements that reference it in
226// one generation.
227func (s *Scene) RemoveAssetCascade(id AssetID) error {
228 if s == nil {
229 return fmt.Errorf("remove asset: %w", ErrAssetNotFound)
230 }
231 s.mu.Lock()
232 defer s.mu.Unlock()
233 candidate := cloneState(s.state)
234 if err := removeAsset(candidate, id, true); err != nil {
235 return err
236 }
237 if err := advanceGeneration(candidate); err != nil {
238 return fmt.Errorf("remove asset: %w", err)
239 }
240 s.state = candidate
241 return nil
242}
243
244// Place adds one sparse placement and returns its opaque ID.
245func (s *Scene) Place(spec PlacementSpec) (PlacementID, error) {
246 if s == nil {
247 return PlacementID{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
248 }
249 s.mu.Lock()
250 defer s.mu.Unlock()
251 candidate := cloneState(s.state)
252 record, err := s.preparePlacement(candidate, spec, nil)
253 if err != nil {
254 return PlacementID{}, err
255 }
256 if uint64(len(candidate.placements)) >= s.limits.MaxPlacements {
257 return PlacementID{}, fmt.Errorf("place: %w", ErrTooManyPlacements)
258 }
259 id, ok := takePlacementID(candidate)
260 if !ok {
261 return PlacementID{}, fmt.Errorf("place: %w", ErrIdentifierOverflow)
262 }
263 record.id = id
264 record.order = takeOrder(candidate)
265 candidate.placements[id] = record
266 candidate.usage.Placements++
267 if err := advanceGeneration(candidate); err != nil {
268 return PlacementID{}, fmt.Errorf("place: %w", err)
269 }
270 s.state = candidate
271 return id, nil
272}
273
274// AddPlacement is an alias for Place.
275func (s *Scene) AddPlacement(spec PlacementSpec) (PlacementID, error) { return s.Place(spec) }
276
277// PlaceAsset creates a full-source placement of an asset.
278func (s *Scene) PlaceAsset(asset AssetID, destination PixelRect) (PlacementID, error) {
279 return s.Place(PlacementSpec{Asset: asset, Destination: destination})
280}
281
282// UpdatePlacement replaces the geometry and optional metadata of a placement.
283// If spec.Asset and spec.AssetID are both invalid, the existing asset is kept.
284func (s *Scene) UpdatePlacement(id PlacementID, spec PlacementSpec) error {
285 if s == nil {
286 return fmt.Errorf("update placement: %w", ErrPlacementNotFound)
287 }
288 s.mu.Lock()
289 defer s.mu.Unlock()
290 candidate := cloneState(s.state)
291 existing, ok := candidate.placements[id]
292 if !ok {
293 return fmt.Errorf("update placement: %w", ErrPlacementNotFound)
294 }
295 record, err := s.preparePlacement(candidate, spec, &existing)
296 if err != nil {
297 return err
298 }
299 record.id = id
300 record.order = existing.order
301 candidate.placements[id] = record
302 if err := advanceGeneration(candidate); err != nil {
303 return fmt.Errorf("update placement: %w", err)
304 }
305 s.state = candidate
306 return nil
307}
308
309// MovePlacement changes only the destination rectangle of a placement.
310func (s *Scene) MovePlacement(id PlacementID, destination PixelRect) error {
311 return s.UpdatePlacement(id, PlacementSpec{Destination: destination})
312}
313
314// RemovePlacement removes one sparse placement.
315func (s *Scene) RemovePlacement(id PlacementID) error {
316 if s == nil {
317 return fmt.Errorf("remove placement: %w", ErrPlacementNotFound)
318 }
319 s.mu.Lock()
320 defer s.mu.Unlock()
321 candidate := cloneState(s.state)
322 if _, ok := candidate.placements[id]; !ok {
323 return fmt.Errorf("remove placement: %w", ErrPlacementNotFound)
324 }
325 delete(candidate.placements, id)
326 candidate.usage.Placements--
327 if err := advanceGeneration(candidate); err != nil {
328 return fmt.Errorf("remove placement: %w", err)
329 }
330 s.state = candidate
331 return nil
332}
333
334// Clear removes every asset and placement while preserving the monotonic ID
335// counters. It publishes a new generation only when the scene is non-empty.
336func (s *Scene) Clear() error {
337 if s == nil {
338 return nil
339 }
340 s.mu.Lock()
341 defer s.mu.Unlock()
342 if len(s.state.assets) == 0 && len(s.state.placements) == 0 {
343 return nil
344 }
345 candidate := cloneState(s.state)
346 candidate.assets = make(map[AssetID]assetRecord)
347 candidate.placements = make(map[PlacementID]placementRecord)
348 candidate.usage = Usage{}
349 if err := advanceGeneration(candidate); err != nil {
350 return fmt.Errorf("clear scene: %w", err)
351 }
352 s.state = candidate
353 return nil
354}
355
356// Apply commits sparse operations transactionally. If any operation fails,
357// neither the state nor the generation changes.
358func (s *Scene) Apply(operations ...Operation) error {
359 if s == nil {
360 return fmt.Errorf("apply operations: %w", ErrInvalidOperation)
361 }
362 if len(operations) == 0 {
363 return nil
364 }
365 s.mu.Lock()
366 defer s.mu.Unlock()
367 candidate := cloneState(s.state)
368 for i, operation := range operations {
369 if err := s.applyOperation(candidate, operation); err != nil {
370 return fmt.Errorf("apply operation %d: %w", i, err)
371 }
372 }
373 if err := advanceGeneration(candidate); err != nil {
374 return fmt.Errorf("apply operations: %w", err)
375 }
376 s.state = candidate
377 return nil
378}
379
380// ApplyOperations is the slice-taking form of Apply.
381func (s *Scene) ApplyOperations(operations []Operation) error { return s.Apply(operations...) }
382
383// Snapshot returns an immutable reference to the current scene state. Future
384// scene mutations use copy-on-write maps and cannot alter this snapshot.
385func (s *Scene) Snapshot() *Snapshot {
386 if s == nil {
387 return &Snapshot{state: emptyState()}
388 }
389 s.mu.RLock()
390 defer s.mu.RUnlock()
391 return &Snapshot{state: s.state}
392}
393
394// Generation returns the current scene generation.
395func (s *Scene) Generation() Generation {
396 if s == nil {
397 return 0
398 }
399 s.mu.RLock()
400 defer s.mu.RUnlock()
401 return s.state.generation
402}
403
404// Usage returns the current live resource usage.
405func (s *Scene) Usage() Usage {
406 if s == nil {
407 return Usage{}
408 }
409 s.mu.RLock()
410 defer s.mu.RUnlock()
411 return s.state.usage
412}
413
414func (s *Scene) prepareAsset(blob AssetBlob, state *sceneState, copyEncoded bool) (assetRecord, error) {
415 data := blob.Encoded
416 if blob.Width <= 0 || blob.Height <= 0 {
417 return assetRecord{}, fmt.Errorf("add asset: %w", ErrInvalidAsset)
418 }
419 pixels, ok := decodedPixels(blob.Width, blob.Height, blob.DecodedPixels)
420 if !ok {
421 return assetRecord{}, fmt.Errorf("add asset: %w", ErrInvalidAsset)
422 }
423 encoded := uint64(len(data))
424 if encoded > s.limits.MaxEncodedBytesPerAsset {
425 return assetRecord{}, fmt.Errorf("add asset: %w", ErrEncodedBudget)
426 }
427 if pixels > s.limits.MaxDecodedPixelsPerAsset {
428 return assetRecord{}, fmt.Errorf("add asset: %w", ErrDecodedPixelBudget)
429 }
430 if uint64(len(state.assets)) >= s.limits.MaxAssets {
431 return assetRecord{}, fmt.Errorf("add asset: %w", ErrTooManyAssets)
432 }
433 if !within(state.usage.EncodedBytes, encoded, s.limits.MaxEncodedBytes) {
434 return assetRecord{}, fmt.Errorf("add asset: %w", ErrEncodedBudget)
435 }
436 if !within(state.usage.DecodedPixels, pixels, s.limits.MaxDecodedPixels) {
437 return assetRecord{}, fmt.Errorf("add asset: %w", ErrDecodedPixelBudget)
438 }
439 // Budgets count len(data). An owned slice with spare capacity would keep
440 // unaccounted memory alive (for example base64 input padded with CR/LF
441 // or a zlib read buffer), so it is trimmed by copying.
442 if copyEncoded || cap(data)-len(data) > len(data)/8 {
443 exact := make([]byte, len(data))
444 copy(exact, data)
445 data = exact
446 }
447 return assetRecord{encoded: data, format: blob.Format, width: blob.Width, height: blob.Height, pixels: pixels}, nil
448}
449
450func (s *Scene) preparePlacement(state *sceneState, spec PlacementSpec, existing *placementRecord) (placementRecord, error) {
451 asset := spec.Asset
452 if !asset.Valid() && existing != nil {
453 asset = existing.asset
454 }
455 recordAsset, ok := state.assets[asset]
456 if !ok {
457 return placementRecord{}, fmt.Errorf("place: %w", ErrAssetNotFound)
458 }
459 destination := spec.Destination
460 if existing != nil && destination == (PixelRect{}) {
461 destination = existing.destination
462 }
463 if !destination.Valid() || destination.Empty() {
464 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
465 }
466 source := spec.Source
467 if source == (PixelRect{}) {
468 if existing != nil && spec.Source == (PixelRect{}) {
469 source = existing.source
470 } else {
471 source = PixelRect{Width: recordAsset.width, Height: recordAsset.height}
472 }
473 }
474 if !source.Valid() || source.Empty() {
475 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
476 }
477 assetBounds := PixelRect{Width: recordAsset.width, Height: recordAsset.height}
478 if _, ok := assetBounds.Intersect(source); !ok || source != assetBounds && !containsRect(assetBounds, source) {
479 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
480 }
481 cells := spec.Cells
482 if existing != nil && !spec.HasCells && cells == (CellRect{}) {
483 cells = existing.cells
484 }
485 if cells != (CellRect{}) && (!cells.Valid() || cells.Empty()) {
486 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
487 }
488 layer := spec.Layer
489 if existing != nil && !spec.HasLayer {
490 layer = existing.layer
491 }
492 return placementRecord{asset: asset, source: source, destination: destination, cells: cells, layer: layer}, nil
493}
494
495func containsRect(outer, inner PixelRect) bool {
496 right, okRight := outer.Right()
497 innerRight, okInnerRight := inner.Right()
498 bottom, okBottom := outer.Bottom()
499 innerBottom, okInnerBottom := inner.Bottom()
500 return okRight && okInnerRight && okBottom && okInnerBottom && inner.X >= outer.X && innerRight <= right && inner.Y >= outer.Y && innerBottom <= bottom
501}
502
503func removeAsset(state *sceneState, id AssetID, cascade bool) error {
504 record, ok := state.assets[id]
505 if !ok {
506 return fmt.Errorf("remove asset: %w", ErrAssetNotFound)
507 }
508 for placementID, placement := range state.placements {
509 if placement.asset != id {
510 continue
511 }
512 if !cascade {
513 return fmt.Errorf("remove asset %s: %w", id, ErrAssetInUse)
514 }
515 delete(state.placements, placementID)
516 state.usage.Placements--
517 }
518 delete(state.assets, id)
519 state.usage.Assets--
520 state.usage.EncodedBytes -= uint64(len(record.encoded))
521 state.usage.DecodedPixels -= record.pixels
522 return nil
523}
524
525func (s *Scene) applyOperation(state *sceneState, operation Operation) error {
526 switch operation.Kind {
527 case OperationAddAsset:
528 record, err := s.prepareAsset(operation.Blob, state, true)
529 if err != nil {
530 return err
531 }
532 id, ok := takeAssetID(state)
533 if !ok {
534 return fmt.Errorf("add asset: %w", ErrIdentifierOverflow)
535 }
536 record.id = id
537 state.assets[id] = record
538 state.usage.Assets++
539 state.usage.EncodedBytes += uint64(len(record.encoded))
540 state.usage.DecodedPixels += record.pixels
541 case OperationRemoveAsset:
542 if err := removeAsset(state, operation.Asset, false); err != nil {
543 return err
544 }
545 case OperationPlace:
546 record, err := s.preparePlacement(state, operation.Placement, nil)
547 if err != nil {
548 return err
549 }
550 if uint64(len(state.placements)) >= s.limits.MaxPlacements {
551 return fmt.Errorf("place: %w", ErrTooManyPlacements)
552 }
553 id, ok := takePlacementID(state)
554 if !ok {
555 return fmt.Errorf("place: %w", ErrIdentifierOverflow)
556 }
557 record.id = id
558 record.order = takeOrder(state)
559 state.placements[id] = record
560 state.usage.Placements++
561 case OperationUpdatePlacement:
562 existing, ok := state.placements[operation.PlacementID]
563 if !ok {
564 return fmt.Errorf("update placement: %w", ErrPlacementNotFound)
565 }
566 record, err := s.preparePlacement(state, operation.Placement, &existing)
567 if err != nil {
568 return err
569 }
570 record.id = existing.id
571 record.order = existing.order
572 state.placements[existing.id] = record
573 case OperationRemovePlacement:
574 if _, ok := state.placements[operation.PlacementID]; !ok {
575 return fmt.Errorf("remove placement: %w", ErrPlacementNotFound)
576 }
577 delete(state.placements, operation.PlacementID)
578 state.usage.Placements--
579 default:
580 return fmt.Errorf("operation kind %d: %w", operation.Kind, ErrInvalidOperation)
581 }
582 return nil
583}
584
585func cloneState(state *sceneState) *sceneState {
586 if state == nil {
587 return emptyState()
588 }
589 clone := *state
590 clone.assets = make(map[AssetID]assetRecord, len(state.assets))
591 for id, record := range state.assets {
592 clone.assets[id] = record
593 }
594 clone.placements = make(map[PlacementID]placementRecord, len(state.placements))
595 for id, record := range state.placements {
596 clone.placements[id] = record
597 }
598 return &clone
599}
600
601func advanceGeneration(state *sceneState) error {
602 if state.generation == ^uint64(0) {
603 return ErrGenerationOverflow
604 }
605 state.generation++
606 return nil
607}
608
609func takeAssetID(state *sceneState) (AssetID, bool) {
610 if state.nextAsset == 0 {
611 return AssetID{}, false
612 }
613 id := AssetID{value: state.nextAsset}
614 if state.nextAsset == ^uint64(0) {
615 state.nextAsset = 0
616 } else {
617 state.nextAsset++
618 }
619 return id, true
620}
621
622func takePlacementID(state *sceneState) (PlacementID, bool) {
623 if state.nextPlace == 0 {
624 return PlacementID{}, false
625 }
626 id := PlacementID{value: state.nextPlace}
627 if state.nextPlace == ^uint64(0) {
628 state.nextPlace = 0
629 } else {
630 state.nextPlace++
631 }
632 return id, true
633}
634
635func takeOrder(state *sceneState) uint64 {
636 order := state.nextOrder
637 if state.nextOrder != ^uint64(0) {
638 state.nextOrder++
639 } else {
640 state.nextOrder = 0
641 }
642 return order
643}
644
645func within(current, additional, limit uint64) bool {
646 if current > limit {
647 return false
648 }
649 return additional <= limit-current
650}
651
652func decodedPixels(width, height int64, declared uint64) (uint64, bool) {
653 if width <= 0 || height <= 0 {
654 return 0, false
655 }
656 w, h := uint64(width), uint64(height)
657 if h != 0 && w > ^uint64(0)/h {
658 return 0, false
659 }
660 computed := w * h
661 // DecodedPixels is retained as an optional compatibility field, but it is
662 // only a valid declaration when it agrees with the dimensions. Resource
663 // accounting must never trust a caller-supplied number that can undercount
664 // the pixels represented by the asset.
665 if declared != 0 && declared != computed {
666 return 0, false
667 }
668 return computed, true
669}
670
671// AssetView is an immutable view of a registered asset. Encoded returns a new
672// byte slice on every call; no caller can mutate scene or snapshot storage.
673type AssetView struct {
674 id AssetID
675 encoded []byte
676 format AssetFormat
677 width int64
678 height int64
679 pixels uint64
680}
681
682// Asset is an alias for the immutable snapshot view.
683type Asset = AssetView
684
685func (a AssetView) ID() AssetID { return a.id }
686func (a AssetView) Format() AssetFormat { return a.format }
687func (a AssetView) Width() int64 { return a.width }
688func (a AssetView) Height() int64 { return a.height }
689func (a AssetView) DecodedPixels() uint64 { return a.pixels }
690func (a AssetView) EncodedSize() uint64 { return uint64(len(a.encoded)) }
691func (a AssetView) Encoded() []byte { return append([]byte(nil), a.encoded...) }
692func (a AssetView) Bytes() []byte { return a.Encoded() }
693func (a AssetView) Blob() AssetBlob {
694 return AssetBlob{Encoded: a.Encoded(), Format: a.format, Width: a.width, Height: a.height, DecodedPixels: a.pixels}
695}
696
697// PlacementView is an immutable view of a sparse placement.
698type PlacementView struct {
699 id PlacementID
700 asset AssetID
701 source PixelRect
702 destination PixelRect
703 cells CellRect
704 layer int64
705}
706
707// Placement is an alias for the immutable snapshot view.
708type Placement = PlacementView
709
710func (p PlacementView) ID() PlacementID { return p.id }
711func (p PlacementView) AssetID() AssetID { return p.asset }
712func (p PlacementView) Source() PixelRect { return p.source }
713func (p PlacementView) Destination() PixelRect { return p.destination }
714func (p PlacementView) Cells() CellRect { return p.cells }
715func (p PlacementView) Layer() int64 { return p.layer }
716func (p PlacementView) Spec() PlacementSpec {
717 return PlacementSpec{
718 Asset: p.asset,
719 Source: p.source,
720 Destination: p.destination,
721 Cells: p.cells,
722 HasCells: p.cells != (CellRect{}),
723 Layer: p.layer,
724 HasLayer: true,
725 }
726}
727
728// Snapshot is an immutable reference to a scene state.
729type Snapshot struct{ state *sceneState }
730
731// Generation is a monotonically increasing scene generation.
732func (s *Snapshot) Generation() uint64 {
733 if s == nil || s.state == nil {
734 return 0
735 }
736 return s.state.generation
737}
738
739// Usage reports the resources retained by this snapshot's state.
740func (s *Snapshot) Usage() Usage {
741 if s == nil || s.state == nil {
742 return Usage{}
743 }
744 return s.state.usage
745}
746
747// Asset returns an immutable asset view by ID.
748func (s *Snapshot) Asset(id AssetID) (AssetView, bool) {
749 if s == nil || s.state == nil {
750 return AssetView{}, false
751 }
752 record, ok := s.state.assets[id]
753 if !ok {
754 return AssetView{}, false
755 }
756 return AssetView(record), true
757}
758
759// GetAsset is an alias for Asset.
760func (s *Snapshot) GetAsset(id AssetID) (AssetView, bool) { return s.Asset(id) }
761
762// Assets returns immutable asset views in ID order.
763func (s *Snapshot) Assets() []AssetView {
764 if s == nil || s.state == nil || len(s.state.assets) == 0 {
765 return nil
766 }
767 ids := make([]AssetID, 0, len(s.state.assets))
768 for id := range s.state.assets {
769 ids = append(ids, id)
770 }
771 sort.Slice(ids, func(i, j int) bool { return ids[i].value < ids[j].value })
772 assets := make([]AssetView, 0, len(ids))
773 for _, id := range ids {
774 asset, _ := s.Asset(id)
775 assets = append(assets, asset)
776 }
777 return assets
778}
779
780// Placement returns an immutable placement view by ID.
781func (s *Snapshot) Placement(id PlacementID) (PlacementView, bool) {
782 if s == nil || s.state == nil {
783 return PlacementView{}, false
784 }
785 record, ok := s.state.placements[id]
786 if !ok {
787 return PlacementView{}, false
788 }
789 return PlacementView{id: record.id, asset: record.asset, source: record.source, destination: record.destination, cells: record.cells, layer: record.layer}, true
790}
791
792// GetPlacement is an alias for Placement.
793func (s *Snapshot) GetPlacement(id PlacementID) (PlacementView, bool) { return s.Placement(id) }
794
795// Placements returns immutable placement views in layer and insertion order.
796func (s *Snapshot) Placements() []PlacementView {
797 if s == nil || s.state == nil || len(s.state.placements) == 0 {
798 return nil
799 }
800 placements := make([]placementRecord, 0, len(s.state.placements))
801 for _, placement := range s.state.placements {
802 placements = append(placements, placement)
803 }
804 sort.SliceStable(placements, func(i, j int) bool {
805 if placements[i].layer != placements[j].layer {
806 return placements[i].layer < placements[j].layer
807 }
808 return placements[i].order < placements[j].order
809 })
810 views := make([]PlacementView, 0, len(placements))
811 for _, placement := range placements {
812 views = append(views, PlacementView{id: placement.id, asset: placement.asset, source: placement.source, destination: placement.destination, cells: placement.cells, layer: placement.layer})
813 }
814 return views
815}
816
817// VisibleFragments returns clipped placement fragments. viewport may be a
818// PixelRect, CellRect, or Viewport. Unsupported or invalid viewport values
819// produce no fragments rather than wrapping coordinates.
820func (s *Snapshot) VisibleFragments(viewport any) []VisibleFragment {
821 if s == nil || s.state == nil {
822 return nil
823 }
824 pixels, cells, hasPixels, hasCells := viewportParts(viewport)
825 if !hasPixels && !hasCells {
826 return nil
827 }
828 placements := s.Placements()
829 fragments := make([]VisibleFragment, 0, len(placements))
830 for _, placement := range placements {
831 visible, ok := visiblePlacement(placement, pixels, cells, hasPixels, hasCells)
832 if ok {
833 fragments = append(fragments, visible)
834 }
835 }
836 return fragments
837}
838
839// VisiblePixelFragments is the typed pixel-space form of VisibleFragments.
840func (s *Snapshot) VisiblePixelFragments(viewport PixelRect) []VisibleFragment {
841 return s.VisibleFragments(viewport)
842}
843
844// VisibleCellFragments is the typed cell-space form of VisibleFragments.
845func (s *Snapshot) VisibleCellFragments(viewport CellRect) []VisibleFragment {
846 return s.VisibleFragments(viewport)
847}
848
849func viewportParts(viewport any) (PixelRect, CellRect, bool, bool) {
850 switch value := viewport.(type) {
851 case PixelRect:
852 return value, CellRect{}, value.Valid() && !value.Empty(), false
853 case CellRect:
854 return PixelRect{}, value, false, value.Valid() && !value.Empty()
855 case Viewport:
856 hasPixels := value.Pixels != (PixelRect{})
857 hasCells := value.Cells != (CellRect{})
858 if hasPixels && (!value.Pixels.Valid() || value.Pixels.Empty()) {
859 return PixelRect{}, CellRect{}, false, false
860 }
861 if hasCells && (!value.Cells.Valid() || value.Cells.Empty()) {
862 return PixelRect{}, CellRect{}, false, false
863 }
864 return value.Pixels, value.Cells, hasPixels, hasCells
865 case *Viewport:
866 if value == nil {
867 return PixelRect{}, CellRect{}, false, false
868 }
869 return viewportParts(*value)
870 default:
871 return PixelRect{}, CellRect{}, false, false
872 }
873}
874
875func visiblePlacement(placement PlacementView, pixels PixelRect, cells CellRect, hasPixels, hasCells bool) (VisibleFragment, bool) {
876 destination := placement.destination
877 if hasPixels {
878 var ok bool
879 destination, ok = destination.Intersect(pixels)
880 if !ok {
881 return VisibleFragment{}, false
882 }
883 }
884 var visibleCells CellRect
885 if placement.cells.Valid() && !placement.cells.Empty() {
886 visibleCells = placement.cells
887 if hasCells {
888 var ok bool
889 visibleCells, ok = visibleCells.Intersect(cells)
890 if !ok {
891 return VisibleFragment{}, false
892 }
893 cellPixels, ok := mapSubRectCellToPixel(placement.cells, placement.destination, visibleCells)
894 if !ok {
895 return VisibleFragment{}, false
896 }
897 var pixelOK bool
898 destination, pixelOK = destination.Intersect(cellPixels)
899 if !pixelOK {
900 return VisibleFragment{}, false
901 }
902 }
903 if hasPixels {
904 var ok bool
905 visibleCells, ok = mapSubRectPixelToCell(placement.destination, placement.cells, destination)
906 if !ok {
907 return VisibleFragment{}, false
908 }
909 if hasCells {
910 visibleCells, ok = visibleCells.Intersect(cells)
911 if !ok {
912 return VisibleFragment{}, false
913 }
914 }
915 }
916 } else if hasCells {
917 return VisibleFragment{}, false
918 }
919 source, ok := mapSubRect(placement.destination, placement.source, destination)
920 if !ok {
921 return VisibleFragment{}, false
922 }
923 return VisibleFragment{PlacementID: placement.id, AssetID: placement.asset, Source: source, Destination: destination, Cells: visibleCells}, true
924}
925
926// mapSubRect maps a non-empty sub-rectangle from one rectangle's coordinate
927// space into another rectangle's coordinate space. Its range mapping uses
928// exact 128-bit arithmetic so products cannot wrap before division.
929func mapSubRect(from, to, sub PixelRect) (PixelRect, bool) {
930 if !from.Valid() || !to.Valid() || !sub.Valid() || from.Empty() || to.Empty() || sub.Empty() || !containsRect(from, sub) {
931 return PixelRect{}, false
932 }
933 x, okX := mapRange(from.X, from.Width, to.X, to.Width, sub.X, mustRight(sub.X, sub.Width))
934 y, okY := mapRange(from.Y, from.Height, to.Y, to.Height, sub.Y, mustRight(sub.Y, sub.Height))
935 if !okX || !okY || x[1] <= x[0] || y[1] <= y[0] {
936 return PixelRect{}, false
937 }
938 return PixelRect{X: x[0], Y: y[0], Width: x[1] - x[0], Height: y[1] - y[0]}, true
939}
940
941func mapSubRectCellToPixel(from CellRect, to PixelRect, sub CellRect) (PixelRect, bool) {
942 if !from.Valid() || !to.Valid() || !sub.Valid() || from.Empty() || to.Empty() || sub.Empty() || !containsCellRect(from, sub) {
943 return PixelRect{}, false
944 }
945 x, okX := mapRange(from.X, from.Width, to.X, to.Width, sub.X, mustRight(sub.X, sub.Width))
946 y, okY := mapRange(from.Y, from.Height, to.Y, to.Height, sub.Y, mustRight(sub.Y, sub.Height))
947 if !okX || !okY || x[1] <= x[0] || y[1] <= y[0] {
948 return PixelRect{}, false
949 }
950 return PixelRect{X: x[0], Y: y[0], Width: x[1] - x[0], Height: y[1] - y[0]}, true
951}
952
953func mapSubRectPixelToCell(from PixelRect, to CellRect, sub PixelRect) (CellRect, bool) {
954 if !from.Valid() || !to.Valid() || !sub.Valid() || from.Empty() || to.Empty() || sub.Empty() || !containsRect(from, sub) {
955 return CellRect{}, false
956 }
957 x, okX := mapRange(from.X, from.Width, to.X, to.Width, sub.X, mustRight(sub.X, sub.Width))
958 y, okY := mapRange(from.Y, from.Height, to.Y, to.Height, sub.Y, mustRight(sub.Y, sub.Height))
959 if !okX || !okY || x[1] <= x[0] || y[1] <= y[0] {
960 return CellRect{}, false
961 }
962 return CellRect{X: x[0], Y: y[0], Width: x[1] - x[0], Height: y[1] - y[0]}, true
963}
964
965func containsCellRect(outer, inner CellRect) bool {
966 right, okRight := outer.Right()
967 innerRight, okInnerRight := inner.Right()
968 bottom, okBottom := outer.Bottom()
969 innerBottom, okInnerBottom := inner.Bottom()
970 return okRight && okInnerRight && okBottom && okInnerBottom && inner.X >= outer.X && innerRight <= right && inner.Y >= outer.Y && innerBottom <= bottom
971}
972
973func mustRight(start, width int64) int64 {
974 right, _ := checkedEdge(start, width)
975 return right
976}
977
978func mapRange(fromStart, fromSize, toStart, toSize, clipStart, clipEnd int64) ([2]int64, bool) {
979 if fromSize <= 0 || toSize <= 0 || clipStart < fromStart || clipEnd < clipStart {
980 return [2]int64{}, false
981 }
982 fromEnd, ok := checkedEdge(fromStart, fromSize)
983 if !ok || clipEnd > fromEnd {
984 return [2]int64{}, false
985 }
986 startOffset := clipStart - fromStart
987 endOffset := clipEnd - fromStart
988 startDelta, ok := ratio(startOffset, toSize, fromSize, false)
989 if !ok {
990 return [2]int64{}, false
991 }
992 endDelta, ok := ratio(endOffset, toSize, fromSize, true)
993 if !ok {
994 return [2]int64{}, false
995 }
996 start, ok := checkedAdd(toStart, startDelta)
997 if !ok {
998 return [2]int64{}, false
999 }
1000 end, ok := checkedAdd(toStart, endDelta)
1001 if !ok || end <= start {
1002 return [2]int64{}, false
1003 }
1004 return [2]int64{start, end}, true
1005}
1006
1007func ratio(offset, numerator, denominator int64, ceil bool) (int64, bool) {
1008 if offset < 0 || numerator <= 0 || denominator <= 0 {
1009 return 0, false
1010 }
1011 hi, lo := bits.Mul64(uint64(offset), uint64(numerator))
1012 divisor := uint64(denominator)
1013 // bits.Div64 requires hi < divisor. Otherwise the quotient cannot fit in
1014 // uint64 and consequently cannot fit in the int64 result type.
1015 if hi >= divisor {
1016 return 0, false
1017 }
1018 quotient, remainder := bits.Div64(hi, lo, divisor)
1019 if ceil && remainder != 0 {
1020 if quotient == ^uint64(0) {
1021 return 0, false
1022 }
1023 quotient++
1024 }
1025 if quotient > uint64(maxInt64) {
1026 return 0, false
1027 }
1028 return int64(quotient), true
1029}
1030
1031func checkedAdd(a, b int64) (int64, bool) {
1032 if b > 0 && a > maxInt64-b {
1033 return 0, false
1034 }
1035 if b < 0 && a < minInt64-b {
1036 return 0, false
1037 }
1038 return a + b, true
1039}