Skip to content

Increase TinyGo's stack size from 14752 -> 16384 - #95

Merged
orsinium merged 1 commit into
firefly-zero:mainfrom
applejag:feature/go-stack-size
Sep 20, 2026
Merged

orsinium merged 1 commit into
firefly-zero:mainfrom
applejag:feature/go-stack-size

Conversation

@applejag

@applejag applejag commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

I have been coding my app to not do any allocations, but I still got all the allocator functions (runtime.alloc, runtime.scanConservative, etc) in the final build.

After some debugging with tinygo -print-allocs I found that it was because of this:

Increasing the maximum stack size from 256 to 1024 by adding either -stack-size=16384B flag to tinygo build or adding "default-stack-size": 16384 to target.json solved the issue for me. Shaving off 7598 bytes from my build:

$ ff build
_bin                24 Kb (-7598)
_hash                  32
_meta                  37
_stats                 26
...

Now this is where my knowledge ends. I know that adding this config upped the limit on function stacks, and got TinyGo to skip adding the allocation code. But I don't know why you set 14752 limit before to begin with. And I don't know the full consequences of increasing it to 16384 (+1632)

@orsinium

Copy link
Copy Markdown
Member

14752 comes from WASM4. It reserves 6560 bytes of RAM at the start for WASM4-specific data with fixed pointers (frame buffer, inputs, etc) and then 8192 bytes of actual stack:

https://github.com/aduros/wasm4/blob/main/cli/assets/templates/rust/.cargo/config.toml

It's not applicable to Firefly since the wasm app memory is fully owned by the app. We don't have direct memory manipulation, only proper wasm host function calls.

Probably, 448 bytes is a bit too much for the stack? What are you allocating?

@applejag

Copy link
Copy Markdown
Contributor Author

Probably, 448 bytes is a bit too much for the stack? What are you allocating?

Yes, that's a lot. But I have this one function that load all of my assets, and so it's a ton of local variables as I'm doing a lot of image sub slicing, so lots of firefly.Point and firefly.Size which are 8 bytes each, and a bunch of SubImage which take up 24 bytes.

It's this function: https://codeberg.org/applejag/digdug/src/commit/74d246387dd941086a5bd8123b19db5ea0aadd37/assets/assets.go#L90-L178

I could probably also split up the function into smaller chunks, as TinyGo or LLVM will inline that code anyways. This stack size limit feels like quite a arbitrary limit.

@applejag

Copy link
Copy Markdown
Contributor Author

Oh ok, correction: so it's not the function stack that it's checking. It's actual object sizes. (I misunderstood the error message)

It's this part here: https://codeberg.org/applejag/digdug/src/commit/74d246387dd941086a5bd8123b19db5ea0aadd37/assets/assets.go#L131-L136

	Players = [4]PlayerSprites{
		playerSprites,
		loader.PlayerSpritesWithColor(&playerSprites, firefly.ColorDarkGreen),
		loader.PlayerSpritesWithColor(&playerSprites, firefly.ColorPurple),
		loader.PlayerSpritesWithColor(&playerSprites, firefly.ColorYellow),
	}

each PlayerSprites is:

type PlayerSprites struct {
	Right [2]firefly.SubImage
	Left  [2]firefly.SubImage
}

I added a small test to show the sizes:

func TestSizes(t *testing.T) {
	subimage := reflect.TypeFor[firefly.SubImage]()
	t.Logf("firefly.SubImage: align=%d, size=%d", subimage.Align(), subimage.Size())
	sprites := reflect.TypeFor[PlayerSprites]()
	t.Logf("PlayerSprites: align=%d, size=%d", sprites.Align(), sprites.Size())
	array := reflect.TypeFor[[4]PlayerSprites]()
	t.Logf("[4]PlayerSprites: align=%d, size=%d", array.Align(), array.Size())
}

checking the size (on 32-bit):

$ GOARCH=386 go test ./assets/ -v -run TestSize
=== RUN   TestSizes
    assets_test.go:77: firefly.SubImage: align=4, size=28
    assets_test.go:79: PlayerSprites: align=4, size=112
    assets_test.go:81: [4]PlayerSprites: align=4, size=448
--- PASS: TestSizes (0.00s)
PASS
ok      codeberg.org/applejag/digdug/assets     0.005s

So that's what's happening. I just had a huge struct.

So instead of using [4]PlayerSprites (which is 448 bytes big), if I only deal with PlayerSprite (which is 112 bytes big) then I stay below the threshold. With the following code I avoid the object size 448 exceeds maximum stack allocation size 256 error with default stack size limits:

	Players[0] = playerSprites
	Players[1] = loader.PlayerSpritesWithColorInto(&playerSprites, firefly.ColorDarkGreen)
	Players[2] = loader.PlayerSpritesWithColorInto(&playerSprites, firefly.ColorPurple)
	Players[3] = loader.PlayerSpritesWithColorInto(&playerSprites, firefly.ColorYellow)

Diff: https://codeberg.org/applejag/digdug/commit/2b6cb50e3dce17e06a0c3d7c09c510867ea42164


With that in mind, yes now I can avoid allocations even without the changes from this PR, in this case. But it could still be nice to add the changes from this PR so if there's other devs that do weird stuff like me, then they have a little more headroom to work with

@orsinium

Copy link
Copy Markdown
Member

Yeah, it's fine. While it's better to not have much on the stack, I don't mind having it a few kilobytes bigger. Thank you for digging into it.

@orsinium
orsinium merged commit e8e4ef9 into firefly-zero:main Sep 20, 2026
3 checks passed
@orsinium

Copy link
Copy Markdown
Member

Stack size is now configurable: #96

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.

2 participants