@@ -6,12 +6,13 @@ These patterns come from real debugging sessions with the libgodc runtime. Follo
66
77## Memory Model
88
9- | Resource | Limit | Notes |
10- | ----------| -------| -------|
11- | Total RAM | 16 MB | Shared with VRAM, sound, OS |
12- | GC Heap | 2 MB × 2 | Semispace collector, 4MB total |
13- | Goroutine Stack | 64 KB | Fixed size, cannot grow |
14- | Large Object Threshold | 64 KB | Objects larger bypass GC |
9+ | Resource | Default build config | Notes |
10+ | ----------| ----------------------| -------|
11+ | Total RAM | 16 MB main RAM | Dreamcast system RAM budget |
12+ | GC Heap | 2 MB × 2 | Default semispace size, configurable |
13+ | Spawned Goroutine Stack | 64 KB | Default fixed size, cannot grow |
14+ | Main Goroutine Stack | 128 KB | KOS main-thread stack by default |
15+ | Large Object Threshold | 64 KB | Objects strictly larger bypass the GC heap |
1516
1617## 1. Pre-allocate During Loading
1718
@@ -56,9 +57,12 @@ func DespawnParticle(index int) {
5657}
5758```
5859
59- ## 2. Respect the 64KB Stack Limit
60+ ## 2. Respect the Default Stack Limits
6061
61- Each goroutine has a fixed 64KB stack. Unlike desktop Go, stacks cannot grow. Deep recursion or large local variables will crash your game.
62+ Spawned goroutines use a fixed 64KB stack by default. Unlike desktop Go,
63+ stacks cannot grow. The main goroutine uses the KOS main-thread stack instead
64+ (128KB by default), but deep recursion or large local variables are still a
65+ bad fit for this runtime.
6266
6367### Bad: Large local arrays
6468
@@ -150,7 +154,8 @@ func GetVisibleEnemies() []Enemy {
150154
151155## 4. Minimize Goroutines
152156
153- Each goroutine consumes 64KB of stack space. 100 goroutines = 6.4MB RAM—40% of total Dreamcast memory!
157+ Each spawned goroutine consumes 64KB of stack space by default. 100 spawned
158+ goroutines = 6.4MB RAM.
154159
155160### Bad: Goroutine per entity
156161
@@ -234,7 +239,7 @@ func DrawHUD() {
234239 scoreText := fmt.Sprintf (" Score: %d " , score) // Allocates!
235240 DrawText (scoreText)
236241}
237- ` ` ` c
242+ ```
238243
239244### Good: Pre-render or avoid strings
240245
@@ -254,21 +259,23 @@ func DrawScore(score int) {
254259println (" Debug:" , value)
255260```
256261
257- ## 7. Large Assets Bypass GC
262+ ## 7. Large Assets Bypass the GC Heap
258263
259- Allocations over 64KB use ` malloc ` directly and are ** not garbage collected** .
264+ Allocations larger than 64KB use ` malloc ` directly and are ** not garbage
265+ collected** .
260266
261267``` go
262- // This 128KB texture is NOT managed by GC
268+ // This 128KB texture is NOT managed by the GC heap
263269texture := make ([]byte , 256 *256 *2 )
264270
265- // It will live forever (or until program exit)
266- // This is usually fine - load assets once, keep forever
267- ` ` ` go
271+ // It is not freed automatically.
272+ // This is usually fine for load- once assets.
273+ ```
268274
269275Implications:
270276- Large slices don't pressure the GC
271277- They also don't get freed automatically
278+ - A manual free path exists via ` runtime.FreeExternal `
272279- Perfect for textures, sounds, level data
273280
274281## 8. Escape Analysis Awareness
@@ -418,8 +425,8 @@ for i := range arr { } // Index iteration
418425small := Vec3 {1 , 2 , 3 } // Value types
419426make ([]T , 0 , capacity) // Pre-sized slices (at init)
420427val , ok := m[key] // Safe map access
421- select { default : } // Yield in loops
422- runtime_checkpoint () // For panic recovery
428+ select { default : } // Yield when no case is ready
429+ runtime_checkpoint () // Establish checkpoint before deferred recover
423430```
424431
425432### AVOID (during gameplay)
@@ -431,49 +438,25 @@ new(T) // For small types
431438go func () {}() // Excessive goroutines
432439string + string // String concatenation
433440fmt.Sprintf () // Formatted strings
434- recover () // Use runtime_checkpoint instead
441+ recover () // Not enough without a checkpoint
435442for { busyWork () } // Loops without yielding
436443```
437444
438- ## 11. Panic/Recover Limitation
439-
440- Standard Go's ` recover() ` does ** not work** on Dreamcast due to ABI differences. Use the ` runtime_checkpoint() ` pattern instead:
441-
442- ### Bad: Standard recover (won't work)
443-
444- ``` go
445- func SafeCall () {
446- defer func () {
447- if r := recover (); r != nil { // NEVER catches panics!
448- println (" recovered" )
449- }
450- }()
451- panic (" oops" )
452- }
453- ```
454-
455- ### Good: Use runtime_checkpoint
456-
457- ``` go
458- import _ " unsafe"
445+ ## 11. Panic Recovery Is Limited
459446
460- // go:linkname runtime_checkpoint runtime.runtime_checkpoint
461- func runtime_checkpoint () int
447+ libgodc implements ` recover() ` , but resumed execution currently depends on a
448+ checkpoint established before the code that may panic. A recovered panic
449+ longjmps back to that checkpoint.
462450
463- func SafeCall() (recovered bool) {
464- defer func () {
465- if runtime_checkpoint () != 0 {
466- recovered = true
467- return
468- }
469- // Normal cleanup here
470- }()
471- panic (" oops" )
472- return false
473- }
474- ` ` ` go
451+ Practical rules:
452+ - Plain ` recover() ` without a checkpoint is not enough.
453+ - Nil dereference, bounds, and divide-by-zero helpers currently go through the
454+ same panic machinery as ` panic() ` .
455+ - Fatal ` runtime_throw() ` paths and interface type-assertion panic helpers
456+ still abort immediately.
457+ - For gameplay code, avoid panic-based control flow and validate inputs early.
475458
476- Most game code shouldn't need recover . Design to avoid panics:
459+ Most game code shouldn't need recovery . Design to avoid panics:
477460- Check bounds before indexing
478461- Validate inputs at entry points
479462- Use ` ok ` form for map access: ` val, ok := m[key] `
@@ -483,10 +466,10 @@ Most game code shouldn't need recover. Design to avoid panics:
483466The Dreamcast scheduler is ** cooperative** , not preemptive. Goroutines run until they yield.
484467
485468### Goroutines yield when they:
486- - Send/receive on channels
487- - Call ` select ` (including with ` default ` )
488- - Call explicit yield functions
489- - Block on I/O
469+ - Block on channel operations
470+ - Call ` select ` / ` default ` when no case is ready
471+ - Call explicit yield functions such as ` runtime.Gosched() `
472+ - Sleep or wait on timers
490473
491474### Bad: Infinite loop without yielding
492475
@@ -533,7 +516,8 @@ Because of cooperative scheduling:
533516
534517## 13. Select with Default
535518
536- ` select ` with ` default ` is an efficient polling pattern that yields correctly:
519+ ` select ` with ` default ` is an efficient polling pattern that yields when no
520+ case is ready:
537521
538522``` go
539523func pollChannels () {
@@ -545,7 +529,7 @@ func pollChannels() {
545529 handleResult (result)
546530 default :
547531 // No message ready - yields to other goroutines
548- // then returns immediately
532+ // and then returns immediately
549533 }
550534
551535 // Can do other work here
@@ -563,26 +547,25 @@ This pattern works well for:
563547
564548### Goroutine Leak
565549
566- Dead goroutines retain ~ 160 bytes each (G struct only). The stack memory and
567- TLS are properly reclaimed, and the G struct is kept in a free list for reuse
568- by future goroutines. When you spawn a new goroutine, it reuses a G from the
569- free list if available.
550+ The runtime contains a dead-goroutine queue and a ` freegs ` reuse path, but in
551+ the current source exited goroutines do not age into reclaimable state because
552+ ` global_generation ` is never advanced.
570553
571- If you spawn 10,000 goroutines that all exit without spawning new ones, you'll
572- have ~ 1.6MB in the free list. This memory is reused when you spawn new
573- goroutines. Monitor goroutine count with ` runtime.NumGoroutine() ` .
554+ In practice, high-churn spawn/ exit patterns can retain goroutine state instead
555+ of recycling it promptly. Prefer long-lived goroutines and monitor goroutine
556+ count with ` runtime.NumGoroutine() ` .
574557
575- ### Unrecoverable Runtime Panics
558+ ### Panic Recovery Boundary
576559
577- User ` panic() ` is recoverable. Runtime panics are not:
560+ ` panic() ` participates in the panic/recover machinery, and nil/bounds/divide
561+ helpers currently do too.
578562
579- - Nil pointer dereference
580- - Array/slice bounds check
581- - Integer divide by zero
582- - Stack overflow
563+ The hard boundary is elsewhere:
564+ - ` recover() ` without an earlier checkpoint is fatal
565+ - ` runtime_throw() ` failures abort immediately
566+ - Interface type-assertion panic helpers abort immediately
583567
584- These crash immediately. A bounds check failure means program invariants are
585- violated—continuing would corrupt data.
568+ Treat panic recovery as a specialized escape hatch, not a normal game-code tool.
586569
587570### 32-bit Pointers
588571
@@ -620,8 +603,8 @@ dcache_inval_range((uintptr_t)ptr, size); // After DMA read (HW -> CPU)
620603
621604- **reflect**: Basic type inspection only, no `reflect.MakeFunc`
622605- **unsafe**: Works, but remember 4-byte pointers
623- - **sync**: Mutexes work, but with M:1 scheduling no other goroutine runs
624- while you hold a lock—deadlock is impossible but starvation is easy
606+ - **sync**: Mutexes work, but deadlocks and starvation are still possible.
607+ Avoid blocking or sleeping while holding locks.
625608
626609### Compatibility
627610
@@ -658,12 +641,13 @@ If your game crashes:
6586411. **Stack overflow**: Reduce recursion, shrink local arrays
6596422. **Nil pointer**: Check slice bounds, map existence
6606433. **GC corruption**: Ensure pointers are valid (not into freed memory)
661- 4. **Panic without checkpoint**: Use `runtime_checkpoint()` for recovery
644+ 4. **Panic recovery mismatch**: Recovery is checkpoint-based; plain `recover()`
645+ is not enough
662646
663647## Further Reading
664648
665- - `docs/DESIGN .md` - Runtime architecture
666- - `docs/KOS_WRAPPERS .md` - Hardware access
649+ - `docs/reference/design .md` - Runtime architecture
650+ - `docs/reference/kos-wrappers .md` - Hardware access
667651- `examples/` - Working game examples
668652
669653Console development is the art of saying 'no' to malloc.
0 commit comments