Log In  
Follow
shy
Follow

Posted this in the discord, but decided it was worth a repost here. If you're new to binary math, this is a decent introduction of the concepts, and how you can use them in Pico-8.

Introduction to binary numbers

Numbers in pico-8 are stored in binary as 16.16 (32 bits, half above the decimal point, half below). A good way to visualize these numbers is like this:
... 128 64 32 16 8 4 2 1 . 1/2 1/4 1/8 1/16 ...

Thus, 1001.01 is how the number 9.25 actually looks in binary. To convert whole numbers, use your fingers and count powers of two until you hit the last one less than your number, then subtract that much from the number and repeat the process (so 47 is 32+8+4+2+1 == 101111).
pico-8 lets you type numbers as binary (0b101111) or hexadecimal (0x2f), but they're all the same as 47. Use whichever conversion method makes it easiest for you.

Negative numbers are a whole other thing involving two's complement (so -1 is 0b1111111111111111 in Pico-8, because then adding 1 to it makes it zero). Probably not worth mentioning if you're just starting out, but something to keep in mind.
Now, with that out of the way, let's talk binary ops...

Binary operations

The important part about dealing with binary ops is that you have to remember that everything is powers of two. 1 is 0001, 2 is 0010, 4 is 0100, 8 is 1000, so these numbers will become very important for checking if a single bit is on or off. Your basic operations for changing bits are these:

(number & 0b0010)!=0 --is bit 2 on?
number = number | 0b0100 --turn bit 3 on
number = number & 0b1111111111111011.1111111111111111 --turn bit 3 off (but save the rest!)
number = number ^^ 0b1000 --toggle bit 4 on/off (make it the opposite of what it was, 0 or 1)

Shifting operators are great, because they're basically like multiplying or dividing by powers of two, but faster for the computer to use. They let you add bits together in the same number, by moving them to the same "slots". 0001<<3 == 1000, and 1000>>2 == 0010. Remember also that the way these numbers are stored means that <<3 is the same as multiplying by 2^3, or 8. >>2 is the same as dividing by 2^2, or 4.

The difference between a "logical right shift" and an "arithmetic right shift" has to do with what gets shifted into the empty slots. Remember that two's complement weirdness where -1 was stored as 1111111111111111? Well, >> preserves the top bit, so it still divides negative numbers properly by powers of two. >>> just puts 0s into the empty slots.

If you're working with bits and not math, you usually want the >>> operator instead.

Rotation is only really useful in rare cases, in my experience. Some random number generators use it because it doesn't discard anything or replace stuff with zeros, which are obviously less-random ways to adjust your data.

How to use all this

The most common case of bitwise ops is packing multiple numbers into the same number. If you only need numbers between 0 and 15, they can be stored in 4 bits, so there's 28 bits just sitting around doing nothing in each pico-8 number! You can therefore use shifting and bitwise ORs to make a number hold more than one piece of information inside it, which saves space. And that's actually how the screen pixels are stored in memory, since they can only have 16 color values per pixel. If you peek or poke into the screen memory at 0x6000 and above, you'll need to understand how to do some bit shifting to put the colors in the right places.

Just one last thing for bitwise, and then I'll end my TED talk. If you need binary flags (on or off values), you can obviously store that in just one bit. Things like pico-8's sprite flags are stored in this way, so checking bits by &'ing with powers of two will tell you if those individual bits are on or off directly. With the set-on or set-off examples I put above, you can adjust those true/false flags as well.

fget(spr,3) is basically the same as (flags[spr]&(0b00000001<<3))!=0.
That might seem more complicated in expression syntax, but if you go through it this should make sense.

Also note that I wrote a lot of binary syntax with leading zeros for clarity, but you don't have to do that. I much prefer hex myself, and when you get comfortable with it that becomes much easier to use. The conversion between hexadecimal and binary is very easy, because every four 0000's just become a number from 0-f, similar to how the numbers 0-9 are repeated to store bigger numbers like 10 or 100. It's probably outside of the scope of an introduction to these concepts, but once you learn this you have another tool you can use.

If you're interested in experimenting with any of this, I strongly recommend that you check out the Memory section of the Pico-8 manual. There are lots of cool functions to use there!

P#77830 2020-06-09 00:18

So this is probably going to be a long, weird post, and is inspired by this post by @Mot a few weeks ago. I kind of dislike the way that Pico-8's cartridge RAM and Lua RAM exist in different worlds, and how everyone eventually uses huge strings to store data on the Lua side, when they run out of token space. It's fine to do that, and in fact I love that the option exists, but is it really a good default way for people to bypass the Pico-8 limitations? I have an idea I want to float to the community about this.

My proposal is to add two functions to the API, that convert between a Lua table and a cartridge bitstream. Using them to get your token count down would be a pre-publishing task, but it could be automated so that you only have to comment out the table to publish your game:

table = { true, x=15.5, [0]="yes", {"why?"}={"because","lua","supports","this","too"} }
len = luastore(0x0,table[,max_length]) --stores at 0x0 in cartridge RAM (not ROM), returns length of stored data
--cstore(0x0,0x0,len) --ROM is much slower and limits what these two functions can do, so keep it optional
table = luaextract(0x0[,len]) --use length if provided, otherwise it's at the start of the bitstream

In addition to giving people a standard way to store things without wasting a large number of tokens decoding the bitstream, this also does a lot to connect the Lua and cartridge worlds, and makes things like saving games trivial:

luastore(0x5e00,save_data_table,256)
save_data_table = luaextract(0x5e00) --length is explicitly encoded at the start of the bitstream

But in addition to those advantages, it also gives people an opportunity to store data in a compressed bitstream that is standardized across cartridges, and gives a real use case for the 0x4300-0x5dff user data section of memory as temporary Lua storage! Writing your own encoders and decoders can be fun, but a fast, internal system that doesn't use your limited token space would open up this kind of development for a lot more newcomers. I think it caters to the idea of a cozy development environment, since it makes hitting that token limit barrier easily fixable for many creators, and less demoralizing.

Demo implementation

The rest of this post is a sample implementation of how Lua data could be serialized quickly, but with a decent amount of compression so that it works for the intended purpose. I did this more for fun than anything, and I'm sure there are people here that could do a better job of this, but hopefully this explains what I was thinking:

compressed bitstream storage format, for types: NUMBER, STRING, BOOLEAN, TABLE
(do not store types: NIL, FUNCTION, CFUNCTION, USERDATA, THREAD)

All bitstreams start with an implicit 16-bit header, representing the total number of bytes stored. This is more a safety measure than anything else, and can be bypassed with a clever destination offset of 2.

All other LENGTHs are stored as packed ints: [2 bits: coded length in nibbles][4-16 bits: value]

STRINGs are stored as: [LENGTH][x bytes: one per character]

DATA is stored as: [4 bits: number flags][2 bits: type code, if non-number][type-specific data...]
Number flags prioritize compressing numerical data first, since it is assumed most common.

NUMBER DATA is stored as: [non-zero in 0xff00][non-zero in 0xff][non-zero in 0x.ff][non-zero in 0x.00ff][1-4 bytes: value]
0000 in number flags = non-number type follows

Two-bit type codes define less common types (6 total bits to declare these data types):
00 = FALSE DATA (end of entry)
01 = TRUE DATA (end of entry)
10 = STRING DATA (STRING storage follows)
11 = TABLE DATA (TABLE storage follows)

TABLEs are stored as: [2 bits: key flags][string keys+data][number keys+data][non-number/string keys+data]
Key flags optimize what follows in the table:
00 = empty table (end of entry)
10 = string keys only
01 = number keys only
11 = all keys, including BOOLEAN and TABLE keys (for full Lua support, more than for usefulness)

If STRING key flag is active, that section is stored as: [LENGTH of string keys][STRING][DATA][STRING][DATA]...

If NUMBER key flag is active, that section is stored as: [LENGTH of number keys][NUMBER][DATA][NUMBER][DATA]...
Since all keys in this section are numbers, [0000] in number flags is shorthand meaning "previous key + 1". If [0000] is the number flags of the first entry, it is assumed to be the first default key in a table, 1.

If both key flags are active, the final section is stored as: [[2 bits: type code][type DATA...][DATA]]...10
Since all keys in this section are not numbers, 4-bit number flags of DATA type are always omitted for keys.
Since type code 10 (STRING) is impossible in this section, those two bits signify the end of the table structure.
Therefore, in the false-positive case of only NUMBER and STRING keys, this section is only two bits: 10

demonstration encoding:
luastore(0x5e00,{1,[0]=0,str=5.25,[true]=false,{"what"}="why?"}) becomes this bitstream:
[0000|11|11]: [00|0001]:[00|0011][24 bits:"str"]=[0110|00000101|01000000];
              [00|0010]:[0000]=[0100|00000001],[0100|00000000]=[0100|00000000];
              [01]=[0000|00]
              [11|01]:{[00|0001][0000]=[000010][00|0100][32 bits:"what"]}=
                      [000010][00|0100][32 bits:"why?"]
              [10] - end of typed key table, and end of table overall
[table w/all]:[1 str key]:[3 chars:"str"]=[num type:5.5];
              [2 num keys]:[default key]=[num:1],[num:0]=[num:0];
              [bool:true]=[bool type:false],
              [table w/num]:{[1 num key]:[default key]=[str type|4 chars:"what"]}=
                      [str type|4 chars:"why?"]
              ; = 27.75 bytes, store as 30 total bytes in 2-byte integer at bitstream start

I'm interested to hear if people like this idea, and if @zep thinks it would add something to the PICO-8 API. I know it just hit Beta and this is unlikely to be implemented, but I do think it would add a lot of value for many people in the community, myself included.

P#77321 2020-05-28 16:22 ( Edited 2020-05-28 18:28)

I've been looking into building a portable PICO-8 recently, so I'm going through display component websites for a square or nearly-square screen. This website (link) has a 60fps, 4" square touch display, so I was wondering if anyone has any experience with the company, Pimoroni.

They mention on the store page that it "works really well for Pico-8 games", which I think is pretty great, if it's made specifically for what I want to build. I thought @zep mentioned somewhere recently that he was looking for a square screen like this, too, so if the company is good then this may be a starting point for an official PICO-8 portable?

Here's a short video clip of the display in use that I found on another website. Pimoroni seems to be a reseller, but they've added code to get it working on Raspberry Pi, and I can't find anything online about "HyperPixel" that doesn't link back to them. I'm new to builds like this, so if anyone has experience with these kinds of projects, I could use some advice.

P#75955 2020-05-06 12:50

Follow Lexaloffle:        
Generated 2020-07-10 07:29 | 0.095s | 4194k | Q:17