Log In  
Follow
carlc27843
Follow

Cart #cursedconsole-0 | 2021-09-29 | Code ▽ | Embed ▽ | No License
7

Plot

Oh mighty @zep, a quest for thee: to fix this bug in oh-two-three.
Time's very fabric has been torn, and monsters from the past reborn!
A pumpkin army's impossible roll befouls our once cozy console.

Its humbled clock refunds too much, permitting Cauldron's villain's touch.
Relentlessly they spin and taunt, they dare thee to remove their haunt.
Please help us take our console back and save us from their cycle hack!

Their jeering eyes provide the clue, all stars show what you must do.
A table's count below sixteen, as low as any number's been,
Negate it and they'll mock you more... the wellspring of this wretched flaw!

About

An entry in 1023 chars to the #pico1k jam. See itchio page for the cart frozen using PICO-8 0.2.3, in case zep completes his quest and this no longer works in later versions of the console. ;)

The cart demonstrates a virtual CPU exploit in PICO-8 v0.2.3, as ordinarily the fantasy console would not be fast enough to do the rendering at 30fps letalone 60fps. Since we're breaking out of the virtual machine limit the speed will depend on your device. The standalone PICO-8 app runs at 60fps on my dekstop, but the web player is smoother at 30fps. Note the cart played above is a slightly earlier version in 1019 chars which runs at 30fps.

Before we delve into the deciphered demo, feast your eyes on this grisly gist of the final code. You should be able to copy and paste this into your rebooted console and run it. Note I had to rewrite a tab character in one of the strings as \t to get around the lexaloffle website mangling it.

1023 Character Crafty Code

_set_fps(60)๐˜ฉ=64โ˜…={}s=.001๐˜ด=sin
while(#โ˜…>=0)add(โ˜…,โ˜…)
poke(24337,ord('โ–‘โด์›ƒ\t\n♥โฌ‡๏ธโฌ‡๏ธ³³โฌ…๏ธโŒ‚\0\0',1,14))๐˜ข=abs
fillp(โ–’\1)๐˜ค=cos?"โถc0โถwโถt  แถœeโ˜…"
function ๐˜ญ(๐˜บ)all(โ˜…)for t=0,โ–ˆ,s do
w=r-r/3*๐˜ด(t*7%โ–ˆ)x=w*๐˜ค(t)๐˜น=๐˜ข(x)z=w*๐˜ด(t)l=(x*x+y*y+z*z)^.5b=1+๐˜ข(x/5+y/3+z)/l*5
if(y>=11-x^2/200and y<=27-x^2/83+๐˜น*.1or pget(๐˜น,-y)>0)b=8
sset(๐˜ฉ+x,๐˜บ+y,b+((b+โ–ˆ)\1-b\1)*8)end end
y=0while(y<9)r=2+y/16๐˜ญ(22)y+=1
a=0while(a<.43)d=30+15*๐˜ข(๐˜ด(a))y=d*๐˜ค(a)r=d*๐˜ด(a)๐˜ญ(๐˜ฉ)a+=s
function p(v)poke2(d,v)d+=๐˜ฅ end
๐˜ฅ=2๐˜ฎ=12280d=๐˜ฎ for i=1,84do n=ord('@/\0ใ€@)t๐˜ธแถœแถœใ‚‰แถ แถ แต‰แต‰โ˜โ˜ใโ™โ™ใโ—โ—โ– โ– ใ‚‰โ—โฌ…๏ธแต‰แต‰…ใ‚จใ›ใ‚‰ใ‚ใฌ$+07<70+ใฏ$,08<80,ใƒฃโ—ใป$)05<50)ใฏ$*06<60*&+2โ—†72โ—†ใ›*ใƒฃใฏ',i)
if(n<128)p(n)d-=1else for _=0,n\16%8do p(%(d-n%16*2-2))end end
p(-32384)p(770)
d+=316๐˜ฅ=68v=7169p(v+24)p(v+32)p(v)p(v)music()
๐˜ณ=0function ๐˜ฑ()p(%[email protected])c+=2end
::_::?"โถ1"
while(๐˜ณ-8&31!=stat(20))d=12800+๐˜ณ%32*2a=๐˜ณ%128+๐˜ฎ+8c=๐˜ฎ ๐˜ฑ()๐˜ฑ()a+=128๐˜ฑ()๐˜ฑ()๐˜ณ+=1
a=t()*.1c=๐˜ค(a)s=๐˜ด(a)๐˜ป=s+2for y=-๐˜ฉ,๐˜ฉ do all(โ˜…)for x=-๐˜ฉ,๐˜ฉ do v=๐˜ฉ+(s*x+c*y)*๐˜ป&127
๐˜ท=v-73if(๐˜ท>0)v-=๐˜ท*๐˜ด(a*16)/2
k=sget(๐˜ฉ+(c*x-s*y)*๐˜ป&127,v)d=k\8k%=8if(k>0and v<30)k+=7
pset(๐˜ฉ+x,๐˜ฉ+y,k+(k+d)*16)end end goto _

Scintillating Source

Now let's rename the identifiers, remove variables that exist only to reduce code size, convert the strings to bytes, expand some shorthand ifs, whiles and prints, unfold some constants, and format it legibly to explain what it's doing. Once again you should be able to copy and paste this into your console and run it.

-- The standalone PICO-8 app may handle 60fps, depending on your device.
-- Remove this to run at 30fps.
_set_fps(60)

-- The "all star" virtual CPU exploit!
--
-- PICO-8's all(c) function very nicely refunds some virtual cpu cycles via 
-- "_refund_cpu_((#c >= 16) and -16 or -#c", where _refund_cpu is an
-- internal function that actually adds to the virtual cycles consumed;
-- so a negative argument will refund cycles (cart goes faster), 
-- and a positive argument will consume cycles (cart goes slower).
--
-- all(c) where #c==0 causes no extra cycles to be refunded or consumed.
-- all(c) where #c>0 and #c<16 results in #c cycles being refunded.
-- all(c) where #c>=16 results in only 16 cycles being refunded.
--
-- To exploit _refund_cpu and achieve undeserved cycles refunded we
-- need #c to be less than 16 but -#c to be negative and large.
--
-- Negative numbers don't seem to help, even if we could make a negative
-- sized table. -#c will just be positive so _refund_cpu will actually 
-- consume extra cycles and the cart will go slower!
--
-- Fortunately for us, in 1945 John von Neumann anticipated humanity's
-- need to exploit fantasy consoles 76 years hence and prankishly proposed
-- using twos complement to represent negative numbers in future so-
-- called "electronic stored-program digital computers".
--
-- In PICO-8, 0x8000 == 32768 == -32768 == -0x8000. So if we can make
-- #c==-32768 then all(c) will call _refund_cpu(-#c == -32768) and John's 
-- prescient plan will reach its triumphant culmination.
--
-- In lua the __len operation on a table's metatable will be called by #c.
-- So we could do:
--
-- c={__len=function() return -32768 end}
-- setmetatable(c,c)
--
-- but we can do it in fewer characters if we have plenty of memory:
star={}
while(#star>=0)add(star,star)
-- since 32767+1 == -32768 this loops until #star == -32768.
-- now all(star) will call _refund_cpu(-32768) and time will flow backwards!
--
-- Note: it doesn't matter what we add to the table but add (โ˜…,โ˜…) looks
-- spookily like a malevolent pumpkin staring hungrily through the depths of 
-- time...

-- Set the screen palette, equivalent to pal({...},1) but shorter.
poke(0x5f11,ord("\x84\x04\x89\x09\x0a\x87\x83\x83\x03\x03\x8b\x8a\x00\x00",1,14))

-- The cart's palette is:
-- Color 0 is unassigned as it defaults to zero (black), as desired.
-- Colors 1-6 define a brown/orange/yellow gradient for the pumpkin's skin.
-- Colors 7-12 define a green gradient for the pumpkin's stalk.
-- Color 13 is black because the approximate math behind the pumpkin actually
-- generates a 7th gradient color in the top-middle pixel of the stalk,
-- and setting that to black results in a pleasing "fork" in the stalk, if
-- you look closely and squint a bit.
-- Color 14 is black because PICO-8's default spritesheet has a star drawn
-- in color 7 in the top-left; later we'll see we add 7 to the spritesheet
-- color near the top of the spritesheet, and setting color 14 to black was
-- cheaper than clearing the unwanted pixels.
-- (In retrospect we could have shifted the palette entries up one index
-- and had the gradients start at color 2, to avoid the duplicate blacks
-- and thereby saved two chars).

-- In PICO-8 โ–’ is a variable set to 0b0101101001011010.1 which is a checkered
-- fill pattern. We need to remove the .1 transparency bit using \1.
fillp(โ–’\1)

-- P8SCII code to clear the screen, set wide mode on, set tall mode on, skip
-- a couple spaces, use color 14 (black but non-zero) and draw a star. We will
-- pget these pixels later to cheaply carve the pumpkin's eyes (upside-down).
print("โถc0โถwโถt  แถœeโ˜…")

-- plot_pumpkin_row draws one row of either the pumpkin face or stalk to the
-- spritesheet. We draw the pumpkin to the spritesheet once on startup, then
-- each frame read from the spritesheet to render it to the screen.
-- We iterate angles to derive individual 3d pixel positions on the surface,
-- then completely fake lighting in a mathematically unsound yet surprisingly
-- pleasing way.
-- The resulting pixel is assigned a pair of colors to represent a smoother
-- gradient via the fill pattern. So in fact there are 11 "orange" values 
-- counting in-between colors, plus one for the mouth/eyes, and 12 "green" 
-- values.
-- PICO-8 only stores 4 bits per pixel in the spritesheet, so we actually only
-- store the uncolored lighting value which happens to coincide with the
-- orange gradient colors.
-- When rendering the pumpkin each frame, we check each pixel's y coordinate
-- to determine whether it was a stalk, then shift the color to the green 
-- gradient by adding 7. (This is why that wayward star in the default 
-- spritesheet ends up with color 14, which is why we set index 14 to black.)
function plot_pumpkin_row(yofs)
 -- exploit: accelerate this slow startup code!
 all(star)
 for t=0,.5,.001 do
  -- https://www.shadertoy.com/view/4tBcRV is a great demonstration of this
  -- "pumpkin segment" math to get the distance to the surface from the
  -- vertical axis.
  w=r-r/3*sin(t*7%.5)
  -- x is screen position relative to the origin (middle of the screen)
  x=w*cos(t)
  absx=abs(x)
  -- z is distance from XY plane through pumpkin's center
  z=w*sin(t)
  -- Fake diffuse lighting using surface position as normal and an
  -- unnormalized light vector. The value is put in the range 1-6.
  l=(x*x+y*y+z*z)^.5
  b=1+abs(x/5+y/3+z)/l*5
  -- Check if this pixel is part of the mouth (first expression) or
  -- the eyes (second expression) by reading the star we printed earlier.
  if (y>=11-x^2/200 and y<=27-x^2/83+absx*.1) or (pget(absx,-y)>0) then
   b=8 -- base color is 0, and the high bit=8 indicates a 2-color pattern
  end
  -- If the pixel value's fractional part is >= 0.5, then set the high bit=8
  -- to remember to use a two color gradient.
  sset(64+x,yofs+y,b+((b+.5)\1-b\1)*8)
 end
end
-- Plot the pumpkin stalk to the spritesheet
y=0
while y<9 do
 -- Stalk is thicker at the base
 r=2+y/16
 -- Plot one row of the stalk
 plot_pumpkin_row(22)
 y+=1
end
-- Plot the pumpkin face to the spritesheet
a=0
while a<.43 do
 -- Distance to each horizontal slice
 d=30+15*abs(sin(a))
 -- y coordinate of slice, relative to the screen origin
 y=d*cos(a)
 -- Horizontal radius of slice
 r=d*sin(a)
 -- Plot one row of the face
 plot_pumpkin_row(64)
 a+=.001
end

-- Utility function to poke2 a value to the global 'dst' and advance dst 
-- by 'dststep'. Reused with two different dststep values.
function poke2_step(v)
 poke2(dst,v)
 dst+=dststep
end

-- Uncompress the music data
--
-- The uncompressed music data has some per-channel values (4 words), plus two
-- sequences of 128 note pitches (256 bytes). 
-- The per-channel word values define the SFX instrument, volume and effect. 
-- Each note's pitch is added to this value when it's written to the SFX.
-- Channel 0 is 0x2f40: organ, volume 7, vibrato. (sequence A)
-- Channel 1 is 0x1900: pulse, volume 4, slide. (sequence A)
-- Channel 2 is 0x2940: organ, volume 4, vibrato. (sequence B)
-- Channel 3 is 0x5774: noise, volume 3, fade out. (sequence B, pitch offset -12)
--
-- We use simple LZ style compression customized for the input. All the pitches
-- are in the range 0-63, and serendipitously the per-channel words above also
-- only have bytes less than 0x80, so we use bit 7 to mark offset/length pairs.
-- It turns out the sequences have a lot of repetition, and limiting the offset
-- to multiples of 2 between 2-32 and length to multiples of 2 between 2-16
-- works well and packs into 8 bits.
--
-- The sound data is poked to just before 0x3100 so that the dst pointer is
-- ready to setup music pattern data after uncompressing.
--
-- Counting the compressed pitches plus uncompression code, 177 chars are 
-- used in the final cart, compared to 256 bytes for the raw pitches.
snddata=0x3100-(256+8)
dststep=2
dst=snddata
-- Iterate compressed music data bytes
for i=1,84 do
 n=ord("\x40\x2f\x00\x19\x40\x29\x74\x57\x0c\x0c\xc0\x0f\x0f\x0e\x0e\x14\x14\xa0\x13\x13\xa0\xff\xff\x11\x11\xc0\x86\x8b\x0e\x0e\x90\xcf\xa7\xc0\xbb\xb0\x24\x2b\x30\x37\x3c\x37\x30\x2b\xb3\x24\x2c\x30\x38\x3c\x38\x30\x2c\xfb\xff\xb7\x24\x29\x30\x35\x3c\x35\x30\x29\xb3\x24\x2a\x30\x36\x3c\x36\x30\x2a\x26\x2b\x32\x8f\x37\x32\x8f\xa7\x2a\xfb\xb3",i)
 if n<128 then
  -- This is a payload byte, poke it
  poke2_step(n)
  -- Our utility function writes words, so step back a byte
  dst-=1
 else
  -- This is an offset/length pair; copy from prior uncompressed data, word by word.
  -- (Allows reading words just copied to get repeated subsequences)
  for _=0,n\16%8 do
   poke2_step(%(dst-n%16*2-2))
  end 
 end
end
-- Poke the pattern data; we set up a single pattern that references SFX 0,1,2,3 and loops.
poke2_step(0x8180)
poke2_step(0x0302)
-- Skip dst to 0x3200+64 where we will poke SFX control data
dst+=316
-- Each SFX is 68 bytes
dststep=68
v=0x1c01
-- Channel 0's SFX control is 0x1c19: speed=28, reverb=1
-- Channel 1's SFX control is 0x1c21: speed=28, reverb=1, detune=1
-- Channel 2's SFX control is 0x1c01: speed=28
-- Channel 3's SFX control is 0x1c01: speed=28
poke2_step(v+0x18)
poke2_step(v+0x20)
poke2_step(v)
poke2_step(v)
-- Same as music(0); starts the music
music()

-- sndrow tracks which music row we're about to stream out.
-- sndrow%128 is the note index to read from the music data.
-- sndrow%32 is the SFX row to poke.
sndrow=0

-- poke_note_step is a utility function which reads the per-
-- channel word value, and adds it to the current sequence's
-- pitch value, then writes it to the SFX.
function poke_note_step()
 -- Note: dststep is still 68, so this will advance the dst
 -- pointer to the next SFX.
 -- Add word per-channel value to byte sequence pitch.
 poke2_step(%[email protected])
 -- Advance to the next per-channel word.
 c+=2
end

::mainloop::
 -- Flip
 print("โถ1")
 -- Keep 8 notes ahead of the current music playback row streamed
 -- to SFX 0-3. (If doing dynamic gameplay sounds reduce this to 2)
 -- Note: I tried instead setting up 16 SFX to contain the entire song
 -- but that used more chars than the streaming version. Streaming
 -- would also allow dynamic music - with a few more chars we could
 -- have the channels fade in one at a time like the C64 version of
 -- Cauldron...
 while sndrow-8&31 != stat(20) do
  -- Setup dst to channel 0's (i.e. SFX 0's) current streaming row.
  dst=0x3200 + sndrow%32*2
  -- Address of sequence A's streaming pitch
  a=sndrow%128 + snddata + 8
  -- Address of channel 0's per-channel values
  c=snddata
  -- Channel 0 and 1 share sequence A's pitches
  poke_note_step()
  poke_note_step()
  a+=128
  -- Channel 2 and 3 share sequence B's pitches
  poke_note_step()
  poke_note_step()
  sndrow+=1
 end

 -- Render the pumpkin, pixel by pixel
 a=t()*.1
 c=cos(a)
 s=sin(a)
 zoom=s+2
 -- For each y coord relative to the middle of the screen
 for y=-64,64 do
  -- Exploit: accelerate this slow loop!
  all(star)
  -- For each x coord relative to the middle of the screen
  for x=-64,64 do 
   -- Rotzoom screen x,y to get spritesheet coords u,v
   u=64+(c*x-s*y)*zoom & 127
   v=64+(s*x+c*y)*zoom & 127
   -- Animate the pumpkin's jaw up and down
   jawv=v-73
   if jawv>0 then
    v-=jawv*sin(a*16)/2
   end
   -- Get spritesheet pixel and separate into base color value 'b'
   -- and dither flag 'd'
   b=sget(u,v)
   d=b\8
   b%=8
   -- Recolor the stalk from shades of orange to shades of green.
   -- Note: The stalk actually should start at v=30, but leaving that
   -- row orange fakes a little perspective on top of the center slice
   -- of the pumpkin face!
   -- Note: This is also where that unwanted default 'x' in the 
   -- spritesheet gets recolored to color 14, which we set to black in
   -- the palette to hide it. It's drawn every frame!
   if (b>0 and v<30) then
    b+=7
   end
   -- Plot the pixel on the screen. Note the second color for the dither
   -- pattern goes into the high nibble.
   pset(64+x,64+y,b+(b+d)*16)
  end
 end 
goto mainloop
P#97954 2021-09-29 02:15 ( Edited 2021-09-30 21:03)

In 0.2.2c

poke4(dst,peek4(src,len))

takes 0% CPU. e.g.

poke4(0x6000,peek4(0x0000,2048))

to copy spritesheet to screen for free.

Can we rely on this behavior? (please)

P#90592 2021-04-15 07:18

Cart #amstradchips1-1 | 2021-04-11 | Code ▽ | Embed ▽ | No License
43

Hooked on Amstrad Chiptunes - Volume 1 - Dave Rogers

Experience the glory of some of the most revered 80's CPC/ZX chiptunes from the comfort of your PICO-8 console!

  • Netherworld
  • Zynaps
  • Uridium
  • Cybernoid
  • Cybernoid 2: The Revenge
  • Nebulus
  • Marauder
  • Stormlord
  • Stormlord 2: Deliverance
  • Anarchy
  • Battle Valley
  • Herobotix
  • Turbo Boat Simulator
  • Bear-A-Grudge

Controls

right arrow - next song
left arrow - previous song

Tech

This cart emulates the Amstrad CPC/ZX Spectrum AY-3-8910 audio chip to output chiptunes to PICO-8's 5512Hz 8-bit mono PCM serial buffer. To feed the emulation it contains a sound driver capable of generating AY register inputs across three channels of tone/volume/noise-enable as well as a single shared noise tone.

The sound driver was derived from 13 different CPC games and one ZX game. All these games had their music composed or converted/arranged by (the legendary, in my opinion) J Dave Rogers, who also wrote the sound driver code for the games as was customary for chiptune musicians in the 80's. This common lineage made it reasonable to reverse engineer the Z80 code and generalize the driver to handle all the games' music.

I think it sounds a little better in native PICO-8 rather than on the web player. Understand however that in the CPC the AY chip was clocked at 1,000,000Hz (and 1,773,447Hz on the ZX), and emulators typically default to sample at 44,100Hz. Whereas PICO-8 ticks at 60Hz and the serial buffer samples at 5512Hz. Only so much can be done - the percussion/noise is especially vulnerable to low sampling rates.

The AY emulation code is augmented to support the waveform visualization by tracking zero-crossings - this extra work can be removed if the goal is pure audio.

Without the graphics, the sound driver + AY emulator runs at a consistent 24-25% of frametime on a 60hz cart. This demonstrates that, for the appropriate PICO-8 game, it's reasonable to completely emulate the music and sfx via the serial buffer. Alternatively the sound driver portion could be used to drive PICO-8's sfx/music buffers in real time which would allow a lot more music in the cart with full audio sample rate quality. Or do both and synchronize native sfx/music with generated serial buffer output.

The game logos were compressed with PX9

Edit 4/10/2021

Fixed credits for Herobotix and Battle Valley, and fixed percussion on initial part of Zynaps - thanks to feedback from Dave Rogers!

Edit 4/19/2021

Now on itch.io using a suitably retro CRT effect!

P#90136 2021-04-07 18:11 ( Edited 2021-04-20 08:01)

Cart #impossible-1 | 2021-03-13 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA
97

Impossible Mission R.T.

Discovered #pico8's secret 5512Hz 8-bit digital audio out API.

Created a homage to this legendary 80's masterpiece to celebrate.

Thanks zep!

P#88937 2021-03-13 22:14

The pico8 manual states this about SFX instruments:

"SFX instruments are only retriggered when the pitch changes, or the previous note
has zero volume. This is useful for instruments that change more slowly over time.
For example: a bell that gradually fades out. To invert this behaviour, effect 3
(normally 'drop') can be used when triggering the note. All other effect values have
their usual meaning when triggering SFX instruments."

However it seems that adjacent notes (with the same SFX instrument and with the same pitch and non-zero volume) do cause the instrument to be retriggered when the second note is at index zero in a looping sequence. Also, setting the second note's effect to 3 inverts the behavior and the instrument is continued rather than retriggered, but only when the second note is at index zero.

The attached .p8 cart demonstrates this. It has these SFXs:
0: the SFX instrument
1: pattern looping from 0 to 8, with no effect on the note at index 0. The SFX instrument is retriggered at index 0.
2: pattern looping from 0 to 8, with effect 3 on the note at index 0. The SFX instrument is not retriggered at index 0.
3: pattern looping from 1 to 9, with no effect on the note at index 1. The SFX instrument is not retriggered at index 1.
4: pattern looping from 1 to 9, with effect 3 on the note at index 1. The SFX instrument is retriggered at index 1.
5: pattern looping from 24 to 32, with no effect on the note at index 24. The SFX instrument is not retriggered at index 24.

You have to download the cart and play SFX 1..5 in the SFX editor. It does nothing on the web:

Cart #huyiwodimu-0 | 2020-04-28 | Code ▽ | Embed ▽ | No License
1

This behavior reproduced in version 8_0.2.0d and 8_0.1.12c. I only tried those versions.

P#75534 2020-04-28 04:31 ( Edited 2020-04-28 04:39)

Follow Lexaloffle:        
Generated 2021-12-08 07:03:38 | 0.071s | Q:24