I figured out that you can use strings to set up a lot of content cheaply, and I understand that others are using procedural generation to get the same effect. All well and good, but that only works for static content. If you want a longer game where you can make changes that persist across sessions then the 256 byte limit on permenant storage is crippling. The game I want to make is basically impossible unless I use two or three extra carts JUST for their quarter-of-a-kilobyte save space. That seems a bit silly, especially since I'll be punished for it by not being able to use the BBS.
Could we get a little more room, please? Keep in mind that even carts for the original Gameboy could potentially have up to 8K of save space. I don't think that allowing 1K would violate the spirit of the PICO-8, and it would open up a lot of possibilities.
What kind of data are you trying to save? There's probably a lot of byte-crunching you can do to max out that 256 bytes.
Mostly a bunch of flags to track whether objects in the game have been removed or otherwise dealt with. Lots of objects, one flag each for most of them.
As for byte-crunching, I already took that into account. I know I can get 8 flags per byte if I break it down, but 256 is still not enough.
I feel your pain (and would certainly not complain if the size of save data was increased!) :o/
Sometimes I feel like pico-8 can't decide whether it wants to be a platform simulating a very-restricted 8-bit machine, or a platform that simulates an 8-bit machine that's easy to write for.
Right now, the 8k token limit, the .p8.png compressed size limit, and the save space limit all point towards the former, but the fact that we're writing code in one of the world's laziest and slowest languages, Lua, says exactly the opposite. :)
Personally I'd like to see a bit more save space and at least double the token count. There's currently way, way too much time being devoted/wasted to cheating more data into carts, rather than writing the games.
It also feels, for my code at least, that it discourages intelligent and modular code, because breaking common code out into its own function has so much token overhead that there have to be at least three places it's used, otherwise you have to keep it manually inlined. This seems counter-productive to me.
It's partially the fault of Lua for having high overhead on functions, and also insisting on a 32-bit-aligned token format, which wastes a ton of space. I wrote a custom token format for Lua once that generally took about 1/3 to 1/2 half of the space. Feels weird that our code space is limited by a lazy, oversized token format that no one would actually have implemented on an 8-bit machine.
@DGM_47 note that one additional cart grants you 16kb of storage since you can write in it from 0x0000 to 0x42ff. anyway I don't think you can use another cart's save data unless you run its code.
>> "anyway I don't think you can use another cart's save data unless you run its code."
I can't use cstore and reload to manipulate another cart's permanent storage section? That... doesn't sound right. But if is, that pretty much kills the game I was going to make.
Nevermind, I see it now. Those commands copy from ROM and save data is in RAM. And memcpy doesn't say you can target another cart.
Damn. So much for that idea, then.
you can't manipulate another cart's permanent storage section but you can manipulate its full rom.
here's a snippet that saves 1024 bytes from user data mem section (6192 bytes from 0x4300)
to the other cart's rom (17152 bytes from 0x0000)
for some reason pico8 crashes on my setup, but it also crashes on png exports at times so that should be unrelated.
works ok from an html export.
savefile="savedata" function sload() -- load 1024 bytes -- from save cart rom @0x0000 -- to user mem @0x4300 reload(0x4300,0x0000,1024,savedata) end function ssave() -- save 1024 bytes -- from user mem @0x4300 -- to save cart rom @0x0000 cstore(0x0000,0x4300,1024,savedata) end function _init() sload() cls() x,y=peek(0x4300),peek(0x4301) end -- move cross around, -- press (o)/[z]/[c] -- to save and reset -- cross should stay -- where you left it function _update60() if (btn(0)) x=max(0,x-1) if (btn(1)) x=min(x+1,120) if (btn(2)) y=max(0,y-1) if (btn(3)) y=min(y+1,120) if (btn(4)) then poke(0x4300,x) poke(0x4301,y) ssave() run() end end function _draw() spr(0,x,y) end |
works on the bbs too!?
move cross around,
press (o)/[z]/[c] to save and reset
cross should stay where you left it
move around, reset from menu
cross is back where you saved it
As a counterpoint to Felice's points (which are all quite valid), I think there's something to be said for just having fun in the space we have. Case in point, I made my Splatoon demake in under 2k tokens and under 500 lines of code, not because I was being crazy about optimization, but because my focus was just on keeping it simple and fun. (And I'm making no claim to whether it's "good" or not... haha.) My goals were to have fun making it and make it fun to play.
On the other side of things, to the points Felice brought up, my top-down adventure game is one I can already tell will take a lot of optimization and code-crunching to fit into the cart limits. It's frankly less fun to code than my Splatoon demake, but it does offer more challenge to code. And that challenge in itself is what I'm enjoying.
So I think there's a middle ground. I feel like more of us should have fun making games that are smaller in scope and that are really meant for the constraints of PICO-8. But at the same time, I feel like sometimes the "fun" comes from the challenge of hitting the constraint walls and spending time getting our games to fit into those constraints. Much like this cat.... :D
>> "you can't manipulate another cart's permanent storage section but you can manipulate its full rom."
Changes to ROM with cstore persist across sessions?
If that's the case I probably don't even need another cart. There's plenty of save space here! Thanks for the tip.
MBoffin--
All valid counterpoints. My issue isn't so much with any of the various constraints, but with the seeming mismatches between them.
Some constraints are extremely lax, e.g. a lua table lookup costs 1 cycle, even though it's actually a rather expensive operation on a real CPU. I don't mind this, but it contrasts with the fact that...
Other constraints are extremely strict, e.g. the bare minimum token cost to break out a bit of inline code into its own function, with no arguments or return values, is 5 tokens. Add args and returns and it starts getting prohibitive to break code out. For instance, here's a 2D dot product in both inline and called format, with token counts:
-- doing a 2d dot product inline... -- 0 time(s) costs 0 tokens (duh) d=a.x*b.x+a.y*b.y -- 1 time(s) costs 13 tokens d=a.x*b.x+a.y*b.y -- 2 time(s) costs 26 tokens d=a.x*b.x+a.y*b.y -- 3 time(s) costs 39 tokens d=a.x*b.x+a.y*b.y -- 4 time(s) costs 52 tokens -- vs. -- breaking the math into a dot2() call and calling that... function dot2(a,b) return a.x*b.x+a.y*b.y end -- 0 time(s) costs 17 tokens (for definition) a=dot2(b,c) -- 1 time(s) costs 23 tokens a=dot2(b,c) -- 2 time(s) costs 29 tokens a=dot2(b,c) -- 3 time(s) costs 35 tokens a=dot2(b,c) -- 4 time(s) costs 41 tokens |
So if you don't actually call dot2 at least three times, it's a token loss to break it out. I think that's counter-productive, forcing us to pick and choose what we break out and keep on such a granular level. I routinely have to change it one way or the other because I've changed how many times an operation is done and I'm so desperate for tokens.
Lua costs aren't zep's fault, of course, but it's his choice to limit our Lua tokens based on the notion that they are stored in the half of ram we have no access to at runtime (0x8000...0xffff). Because Lua tokens are 32 bits, which they never would have been on an 8-bit system like Pico-8 is pretending to be, that's really, really limiting our program sizes.
In the limited context of our games, we seldom use the expansive opportunities the massive field sizes in a Lua token offer, so it might be more reasonable to pretend that Pico-8 uses some smaller token size, like 16 bits, which would conceptually offer us 16k tokens in that 32kilobyte space. That would make more sense to me and wouldn't feel so ... off, I guess.
Otherwise I tend to love the constraints on Pico-8. As you say, working inside limits is challenging and that's fun. It's like writing haiku, where the constrained format is half of the fun (or more).
Also worth noting is that this kind of constraint really limits what a junior programmer can do on Pico-8, since they tend to write spaghetti code, and that will bloat to 8k tokens really quickly. Not sure that's good for one of Pico's intended purposes (education) either. That's only a minor issue, though, since they ought to learn to write shorter code... by breaking out their code into functions... but wait, that's expensive, so they won't be encouraged to do the right thing... d'oh... ;)
I don't believe Pico-8's token limit has anything to do with the 32K of virtual RAM that would be above the addressable 32K region if Pico-8 were a 64K machine. It's a coincidence that 8,192 is 1/4th the byte count of 32K. Note that some Lua tokens don't count toward the limit, and these discounts were added after the token limit was set and long after the addressable region was defined at 32K. I wouldn't be surprised if there's something interesting in the currently inaccessible RAM region above 0x7fff, but I'd be very surprised if it was tokens.
The actual physical code limit is compressed chars, defined by the size of the PNG cart format (arbitrary but fixed). Early versions of Pico-8 only had an uncompressed chars limit. This was punishing the use of comments and good variable names, and discouraging the sharing of readable source in published carts. So zep implemented compressed code and the token limit, and doubled the uncompressed char limit. This kept the PNG size the same and made token count the dominant limit for typical code.
Of course, the physical limit is itself arbitrary. Pico-8 could use additional tagged data regions of the PNG file format that don't contribute to the cart label image instead of using its steganographic technique. Or it could use a larger image.
And of course the code space limits are unrelated to the save space limits. I suppose if we want save space to continue to be accessible as addressable RAM, we could make 0x8000-0xffff into save space. :)
I'd swear I actually read somewhere that zep said the upper half of ram was where the cart's "code" lived. Conceptually, it's a 64kbyte cart, with data in the low half and tokenized code in the upper half.
It's true the p8 and p8.png formats actually store ascii code that's tokenized at runtime, but that's just a pragmatic convenience that allows interpreter changes/fixes without rebuilding all carts. Actual hardware of such an era usually wouldn't change, of course, so this little conceptual fib in P8 wouldn't really have been necessary and tokenized/assembled code would be stored directly on the cart.
BTW, I realize most of you know these things. I'm just backing up my own arguments with what I believe the original arguments in zep's head to have been. I'm happy to be corrected if I'm getting it wrong, but I'd want something from the horse's mouth. :)
PS: I think the APNG extension to the PNG format would probably be ideal for storing larger/multiple carts. Most browsers can actually display the A)nimation these days, and those that don't will simply display the backwards-compatible first frame. Creating APNGs is literally as easy as using libpng, given that you apply a patch to libpng and use the result. It might be cool to have a basic animated preview of the game, not just a still. :)
I think zep likes to watch us squirm with our conjectures. :)
I can totally see how a poke-able code region could be a desirable feature that's been partially implemented but left inactive. I just don't see how it can be tokens. I don't think poking tokens would be very usable because each poke would require re-parsing the entire token stream and basically restarting the program. Could it be Lua VM bytecode?
Assuming cart RAM is stored in a contiguous chunk on the heap, one could use a debugger to hunt it down and peek at offset 0x8000 and see if it compares to Lua 5.2 bytecode. Unfortunately my debugger skills are weak and I don't know how to do that with a raw binary. Does GDB have a "search all allocated heap chunks for pattern" feature?
No, no, I don't think tokens actually ended up being stored there. I think he said something about considering self-modifying code at one point, but that it ended up being infeasible. But that may be where the concept I'm referring to began, since they would have had to live somewhere we could address. If so, it may have stuck. But only conceptually, not literally. Or heck, maybe it's literal too.
I took a quick look for pico-8's memory, but I didn't see it and I'm really not in the mood to scour the whole win32 address space. :) I'm not sure it matters since, as I said, I think the 0x8000-0xffff limit is only conceptual, not literal.
@Felice: I was actually watching a video not too long ago where the speaker was mentioning how he poked around the x86 instruction set. Now you're talking memory not cpu, But the method he describes to finding the memory addresses to find the instruction sets on a cpu might help with the memory peeking..
https://www.youtube.com/watch?v=KrksBdWcZgQ
Either way, It was an interesting watch.
I was exaggerating a little. I'm capable of post-mortem debugging a dead game, compiled with the most extreme optimizations enabled and the symbols stripped. Regrettably, this is because I was often responsible for the esoteric bug at the engine level that was breaking the game, so I had little choice but to get good at debugging. It's just... I wasn't in the mood to do stuff like walk the stack and figure out where the heap(s) are. I dunno if zep is using the standard heap, a custom heap, an OS-level heap, multiple heaps, etc.¹ I'm still not in the mood. :) Usually what used to put me in the mood was knowing that 30-50 people on the game's team were relying on me to make sure their title shipped. :)
That said, I watched your suggested video and I do recommend it to people who are still new to memory spelunking and the x86/x64 architectures. I'm not sure how much such a person would understand, but it gives you an idea of just how much funky detail there can be to learn, and how useful a bespoke tool can be to find it all.
¹ - Personally I always put Lua in its own heap, because it allocates so maniacally. It'll fragment a main heap so badly that a game running in limited memory will run out when the total 'free' still _appears to be, like, a third of the heap.
@Felice: Ahh, No I get ya. "Too much like work" we call that. X3
Still, Vids like this are what got me a little less afraid of poking active memory.
[Please log in to post a comment]