Code the Fruit Jam
If you want to customize the Demofruit or just have a look at the code, download the Project Bundle linked below.
// SPDX-FileCopyrightText: 2026 John Park with Claude Opus 4.8 for Adafruit Industries
//
// SPDX-License-Identifier: MIT
//
// Demofruit Fruit Jam Demoscene demo
// music by kqvc
// ═══ DEMOFRUIT — FRUIT JAM DEMOSCENE SAMPLER ═══
// Six demos, one UF2. Boot menu selects; watchdog-scratch reboot launches
// each demo with its ideal display configuration; RESET returns to menu.
//
// STARFIELD — warp field + bouncing chrome text (DVHSTX16)
// PLASMA — palette plasma, software palette (DVHSTX16)
// CHROME SPHERE — dual-core raytracer (DVHSTX16)
// VALLEY RUN — wireframe trench + shaded ship (DVHSTX16)
// ROTOZOOM — texture spin/zoom + lens (DVHSTX16, dual-core)
// THE JUGGLER — Eric Graham 1987, PSRAM playback (DVHSTX16, dual-core)
//
// REQUIREMENTS: Board = Adafruit Fruit Jam RP2350, CPU Speed = 150MHz.
#include <Arduino.h>
#include <new>
#include <Adafruit_dvhstx.h>
#include <Adafruit_GFX.h>
#include <Fonts/FreeSansBold24pt7b.h>
#include <pico/multicore.h>
#include <hardware/watchdog.h>
#include <math.h>
#include "common.h"
#include "audio.h"
#include "leds.h"
// ── Songs: song_0.h = menu, song_1..6.h = demos in menu order ────────────────
// Each is a standard mod2header output (MOD_DATA), wrapped in a namespace.
namespace song0 {
#include "song_0.h"
}
namespace song1 {
#include "song_1.h"
}
namespace song2 {
#include "song_2.h"
}
namespace song3 {
#include "song_3.h"
}
namespace song4 {
#include "song_4.h"
}
namespace song5 {
#include "song_5.h"
}
namespace song6 {
#include "song_6.h"
}
struct SmpSong { const unsigned char *data; unsigned size; };
static const SmpSong g_songs[7] = {
{ song0::MOD_DATA, sizeof(song0::MOD_DATA) }, // menu
{ song1::MOD_DATA, sizeof(song1::MOD_DATA) }, // starfield
{ song2::MOD_DATA, sizeof(song2::MOD_DATA) }, // plasma
{ song3::MOD_DATA, sizeof(song3::MOD_DATA) }, // chrome sphere
{ song4::MOD_DATA, sizeof(song4::MOD_DATA) }, // valley run
{ song5::MOD_DATA, sizeof(song5::MOD_DATA) }, // rotozoom
{ song6::MOD_DATA, sizeof(song6::MOD_DATA) }, // the juggler
};
#include "texture.h" // rotozoom bitmap (global: plain data)
// ── Demo modules, each in its own namespace ──────────────────────────────────
namespace sf {
enum RampPhase { CRUISE, RAMP, WARP, SNAP, RECOVER };
#include "starfield_impl.h"
}
namespace pl {
#include "plasma_impl.h"
}
namespace rt {
#include "raytracer_impl.h"
}
namespace vy {
#include "valley_impl.h"
}
namespace rz {
#include "rotozoom_impl.h"
}
namespace jg {
#include "juggler_impl.h"
}
#include "launcher.h"
// ── Displays ──────────────────────────────────────────────────────────────────
// DVHSTX16 as a GLOBAL — identical to every proven standalone sketch
// (the library does clock setup tied to static construction). One display,
// one begin() per boot. All demos render through it; plasma uses a
// software palette LUT instead of DVHSTX8 hardware palette (runtime
// display construction proved unreliable).
DVHSTX16 display16(ADAFRUIT_FRUIT_JAM_CFG, DVHSTX_RESOLUTION_320x240,
true /* double_buffered */);
static void dispFail() {
pinMode(LED_BUILTIN, OUTPUT);
for (;;) {
Serial.println("FATAL: display.begin() failed");
digitalWrite(LED_BUILTIN, HIGH); delay(150);
digitalWrite(LED_BUILTIN, LOW); delay(850);
}
}
static DVHSTX16 *use16() {
DVHSTX16 *d = &display16;
Serial.println("display16.begin()..."); Serial.flush();
if (!d->begin()) dispFail();
Serial.println("display16 OK"); Serial.flush();
smpPsramFix();
return d;
}
// ── Boot dispatch ─────────────────────────────────────────────────────────────
void setup() {
smpBtnInit();
ledInit();
Serial.begin(115200);
uint32_t t0 = millis();
while (!Serial && (millis() - t0) < 2000) delay(10);
int id = smpBootDemoId(); // -1 = menu; clears scratch so reset -> menu
Serial.printf("boot: demo id %d\n", id); Serial.flush();
switch (id) {
case 0: { // STARFIELD
DVHSTX16 *d = use16();
smpAudioInit(g_songs[1].data, g_songs[1].size);
ledAll(LED_C_STARFIELD); // signature color carried from menu
Serial.println("demo: starfield");
sf::starfieldRun(*d);
}
case 1: { // PLASMA — software palette through the shared display
DVHSTX16 *d = use16();
smpAudioInit(g_songs[2].data, g_songs[2].size);
ledAll(LED_C_PLASMA); // signature color carried from menu
Serial.println("demo: plasma");
pl::plasmaInit();
for (;;) { smpAudioPump(); pl::plasmaTick(*d); }
}
case 2: { // CHROME SPHERE
DVHSTX16 *d = use16();
smpAudioInit(g_songs[3].data, g_songs[3].size);
ledAll(LED_C_CHROME); // signature color carried from menu
Serial.println("demo: raytracer");
rt::rtInit(*d);
multicore_launch_core1(rt::rtCore1Entry);
for (;;) { smpAudioPump(); rt::rtCore0Loop(*d); }
}
case 3: { // VALLEY RUN
DVHSTX16 *d = use16();
smpAudioInit(g_songs[4].data, g_songs[4].size);
ledAll(LED_C_VALLEY); // signature color carried from menu
Serial.println("demo: valley");
vy::valleyInit();
for (;;) { smpAudioPump(); vy::valleyTick(*d); }
}
case 4: { // ROTOZOOM
DVHSTX16 *d = use16();
smpAudioInit(g_songs[5].data, g_songs[5].size);
ledAll(LED_C_ROTOZOOM); // signature color carried from menu
Serial.println("demo: rotozoom");
rz::rzInit();
multicore_launch_core1(rz::rzCore1Entry);
for (;;) { smpAudioPump(); rz::rzCore0Loop(*d); }
}
case 5: { // THE JUGGLER
DVHSTX16 *d = use16();
smpAudioInit(g_songs[6].data, g_songs[6].size);
ledAll(LED_C_JUGGLER); // signature color carried from menu
Serial.println("demo: juggler");
jg::jugglerRun(*d);
}
default: { // MENU
DVHSTX16 *d = use16();
smpAudioInit(g_songs[0].data, g_songs[0].size);
Serial.println("menu"); Serial.flush();
menuRun(*d);
}
}
}
void loop() { /* unreachable — every path above loops forever */ }
How It Works
Each demo began as a standalone sketch and was merged into the sampler launcher. They are listed here in menu order, which is also roughly chronological by technique.
Because the each demo uses a slightly different display configuration, and the DVI HSTX library display object is constructed once at startup, Demofruit uses a reboot launcher. This is a similar technique multi-part Amiga demos used to chain-load segments.
On power-up or after pressing reset, no launch flag is set, so the sketch shows the menu. This includes a tumbling, 3D cube behind green text, with the cube tinted to the highlighted demo's signature color.
Menu navigation
- Button 3 = up
- Button 2 = down
- Button 1 = launch
Selecting a demo writes its ID plus a magic number into the RP2350's watchdog scratch registers (which survive a soft reboot) and triggers a watchdog reboot.
On the next boot the sketch sees the flag, immediately clears it, and constructs that demo's ideal display and runs it forever. Because the flag was cleared at demo start, pressing **Reset** returns you to the menu.
The NeoPixel strip reinforces the UI throughout: in the menu it glows the highlighted demo's signature color; inside a demo it shows the current setting (e.g. Valley Run lights *N* of 5 pixels for the speed preset, in the current wire color). Pixel order is top-to-bottom. A brief blink acknowledges "randomize"-type buttons that have no persistent state to display.
Starfield
The star field is one of the oldest tricks in the demo scene, and it works through a bit of faked perspective. Each star is really just a point with an X, Y, and Z position, where Z represents how far away it is. To draw a star, the code divides its X and Y by its Z: distant stars (large Z) get pulled in close to the center of the screen and drawn small, while near stars (small Z) spread out toward the edges and grow large.
To create the sense of flying forward, every frame simply decreases each star's Z a little. As Z shrinks, that division makes the star rush outward and accelerate, and when it passes the viewer it gets recycled to a new random position far away. That single divide per star is the whole illusion of three-dimensional motion.
On the Fruit Jam this runs at full frame rate because the math is so simple, which leaves plenty of room for the bouncing chrome text layered on top. Rather than allocating memory for each letter, the code reads the font's pixel data directly from the Fruit Jam's flash and plots the glyphs over the moving field, letting them bob and scale independently. The result is the classic "warp speed plus scroller" opener that demos have used for decades.
Plasma
This effect comes from adding several sine waves together — one that varies across the screen horizontally, one vertically, one along the diagonal, and one radiating from a center point. Where those waves reinforce each other you get bright values; where they cancel, dark ones. Summed across every pixel, the overlapping ripples produce those signature organic, lava-lamp blobs. The demoscene's key insight was that you do not need to recompute all that trigonometry every frame. Instead you precompute the wave values into tables once, then animate purely by sliding color-lookup offsets through those tables, a cheap addition instead of expensive sine calls.
The Fruit Jam version keeps that period-authentic approach. It builds the wavetables at startup and, in its lightest mode, animates by rotating a 256-entry color palette rather than touching the image at all.
The original Amiga and VGA effects leaned on hardware palette registers to do this for free; the Fruit Jam instead keeps an 8-bit index map and translates it through a software color lookup table into the 16-bit RGB565 values the display wants.
That software palette pass is so fast on the RP2350 that the demo also offers a fuller mode where the wave map itself is recomputed every frame, letting the plasma physically writhe and drift rather than just cycle colors.
Chrome Sphere
This demo is a real raytracer, the same fundamental technique used to render photorealistic 3D images. For every pixel on the screen, it casts an imaginary ray out from the camera into the scene and asks what that ray hits. If it strikes the checkered floor, the code works out which check square it landed in and shades it accordingly. If it strikes the chrome sphere, the ray bounces, it reflects off the surface and continues on to see what the sphere would be mirroring, which is what gives the ball its shiny, reflective look. The code also traces a second ray from each floor point toward the light to decide whether that spot sits in the sphere's shadow. Raytracing was far too slow for real-time animation in the demoscene era, which is exactly why doing it live here is satisfying.
The Fruit Jam pulls this off by putting both of the RP2350's processor cores to work. The chip has two cores, and this demo hands half the image to each: they raytrace alternating rows of the frame simultaneously, roughly doubling the speed. To keep the frame rate smooth, the scene is traced at a reduced resolution and scaled up to fill the display. As you press the buttons to move the light and camera, the new values are shared between the cores so both stay in sync, and the whole reflective, shadowed scene re-renders continuously in real time.
Valley Run
Valley Run is a flat-shaded 3D fly through in the spirit of the vector landscape demos and games like Star Fox. The trench is a mesh of points laid out in a grid, scrolling toward you to create forward motion. Each of those 3D points is projected onto the 2D screen using the same perspective divide the star field uses: divide by distance so far parts of the trench converge toward a vanishing point. The walls are drawn as wireframe lines connecting the grid, and the fighter ship is built from a handful of triangular faces. Each face is "flat shaded," meaning the code calculates how directly it faces the light and fills the whole triangle with a single brightness, giving that crisp, faceted, low-poly look without the cost of smooth shading.
Because the geometry is simple, the Fruit Jam renders the whole scene on a single core with cycles to spare. The ship's motion is more than decoration: it's knee-like banking and side-to-side strafing are computed each frame, so it leans into its movement naturally. A subtle animation also runs along the trench walls so the corridor appears to breathe as it scrolls. The buttons let you change the wire color, step through five scroll speeds, and randomize the terrain's height and roughness on the fly.
Rotozoom
A "rotozoomer" takes a flat image and rotates and scales it in real time while tiling it infinitely across the screen, and it was a demoscene staple because it looks far more computationally expensive than it actually is. The trick is to work backwards. Instead of asking "where does each pixel of my image end up on screen," the code asks, for every screen pixel, "which pixel of the source image belongs here?" It walks across the screen stepping through the source image at an angle and scale determined by the current rotation and zoom. When the coordinates run off the edge of the 128x128 source, they simply wraparound, which is what tiles the pattern seamlessly. Crucially, this stepping is done with fixed-point math — numbers that use integer arithmetic to represent fractions — because integer operations were, and on a microcontroller still are, much faster than floating point for this kind of per-pixel work.
The Fruit Jam version tiles a 128x128 texture whose power-of-two size lets the wrap-around happen with a single fast bitwise mask instead of a division. Both processor cores share the work of filling the screen. On top of the spinning texture, a translucent "lens" bubble drifts around in a looping orbit, magnifying and blue-tinting whatever is beneath it by resampling those pixels through a spherical distortion, a little flourish that shows off how cheap the underlying sampling really is. You can cycle motion presets, toggle the lens, and randomize the zoom range with the buttons.
The Juggler
The Juggler is a piece of computer graphics history. In 1986, Eric Graham created a raytraced animation of a robot juggling three mirrored balls to showoff what the new Commodore Amiga could do, and it became one of the most famous demos of its era. The catch was that each frame took the Amiga's 7 MHz processor around an hour to raytrace, so Graham rendered the frames ahead of time and then played them back in sequence like a movie. This demo is a faithful port of Graham's original 1987 raytracer code, right down to how it bounces rays off the reflective spheres and the checkered floor, with its recursion depth capped so mirror-in-mirror reflections terminate cleanly.
Demofruit follows Graham's exact strategy of render-first, play-later, just way faster. When you launch the demo, both cores of the RP2350 raytrace 24 frames of a full juggling cycle and store them in the Fruit Jam's 8 MB of PSRAM — you can watch the image build in a "venetian blind" pattern as the two cores fill alternating rows, which takes about 20 seconds. Once all 24 frames are cached, the demo plays them back in a smooth loop, and the buttons let you change the playback speed or pause. A single frame that took Graham's Amiga roughly an hour now renders in about a second and a half. The juggling motion itself — a proper three-ball cascade, with the figure's knees bending and body shifting its weight — was recreated for this port, since the original animation data was never part of the published source code.
Page last edited July 22, 2026
Text editor powered by tinymce.