pico-playground/apps/popcorn/popcorn.c

1932 wiersze
80 KiB
C

/*
* Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
#include <stdio.h>
#include <string.h>
#include "pico.h"
#include "pico/stdlib.h"
#include "pico/scanvideo.h"
#include "pico/scanvideo/composable_scanline.h"
#include "pico/audio_i2s.h"
#include "pico/multicore.h"
#include "pico/sync.h"
#include "pico/sd_card.h"
#include "hardware/divider.h"
#include "platypus.h"
#include "font.h"
#if PICO_ON_DEVICE
#include "hardware/dma.h"
#include "hardware/irq.h"
#endif
#if PICO_ON_DEVICE
//#define SHOW_PERF_COUNTERS
#endif
// found an occasional problem with muse if we don't display any of the lines (timing bug?)... isn't the cause of the clicks tho
// #define DEBUG_UNKNOWN_ISSUE1
// todo seems like a compiler bug to me... if a section is assembler only, it gets the wrong linker attributes
uint8_t __attribute__((noinline)) __attribute__((section(".core1.decompress"))) hack() {
return 0;
}
//#define JUST_READ
CU_REGISTER_DEBUG_PINS(frame_generation, audio_buffering)
// ---- select at most one ---
//CU_SELECT_DEBUG_PINS(frame_generation)
//CU_SELECT_DEBUG_PINS(audio_buffering)
#define __popcorn_critical __after_data("popcorn")
#if PICO_NO_HARDWARE
//#define ENABLE_STRICT_ASSERTIONS
#endif
#ifdef ENABLE_STRICT_ASSERTIONS
#define strict_assert(x) assert(x)
#else
#define strict_assert(x) ((void)0)
#endif
bool debug_on;
#if 0
#define popcorn_debug(format,args...) printf(format, ## args)
//#define popcorn_debug(format,args...) if (debug_on) printf(format, ## args)
#define TOKEN_PASTE(x, y) x##y
#define CAT(x,y) TOKEN_PASTE(x,y)
#define pd_linevar CAT(debug_done_l, __LINE__)
//#define popcorn_debug(format,args...) static bool pd_linevar; if (debug_on || !pd_linevar) { pd_linevar = true; printf(format, ## args); }
#else
#define popcorn_debug(format,args...) (void)0
#endif
static const struct scanvideo_mode vga_mode_320x120_60 =
{
.default_timing = &vga_timing_640x480_60_default,
.pio_program = &video_24mhz_composable,
.width = 320,
.height = 120, // only 120 logical scanlines (since we deal with two pixel rows at a time)
.xscale = 2,
.yscale = 4, // * 4 = 480
};
#define vga_mode vga_mode_320x120_60
//#define vga_mode vga_mode_tft_320x240_60
// for now we want to see second counter on native and don't need both cores
//#if PICO_ON_DEVICE
#define RENDER_ON_CORE0
//#endif
#define RENDER_ON_CORE1
//#define IRQS_ON_CORE1
#ifdef SHOW_PERF_COUNTERS
#include "system.h"
#define NUM_PERF_COUNTERS 8
uint32_t perf_count[NUM_PERF_COUNTERS];
uint32_t perf_contention[NUM_PERF_COUNTERS];
uint perf_base;
static uint16_t bar_chart[2][2][128];
static uint bar_chart_size[2][2];
#endif
#if PICO_ON_DEVICE && defined(VGABOARD_BUTTON_A_PIN)
volatile uint32_t button_state = 0; // sorry graham
static const uint button_pins[] = {0, 6, 11};
uint32_t last_button_state;
const uint VSYNC_PIN = PICO_SCANVIDEO_COLOR_PIN_BASE + PICO_SCANVIDEO_COLOR_PIN_COUNT + 1;
// Registered as GPIO interrupt on both edges of vsync. On vsync assertion,
// set pins to input. On deassertion, sample and set back to output.
void vga_board_button_irq_handler() {
int vsync_current_level = gpio_get(VSYNC_PIN);
gpio_acknowledge_irq(VSYNC_PIN, vsync_current_level ? GPIO_IRQ_EDGE_RISE : GPIO_IRQ_EDGE_FALL);
// Note v_sync_polarity == 1 means active-low because anything else would be confusing
if (vsync_current_level != scanvideo_get_mode().default_timing->v_sync_polarity) {
for (int i = 0; i < count_of(button_pins); ++i) {
gpio_pull_down(button_pins[i]);
gpio_set_oeover(button_pins[i], GPIO_OVERRIDE_LOW);
}
}
else {
uint32_t state = 0;
for (int i = 0; i < count_of(button_pins); ++i) {
state |= gpio_get(button_pins[i]) << i;
gpio_set_oeover(button_pins[i], GPIO_OVERRIDE_NORMAL);
}
button_state = state;
}
}
void vga_board_init_buttons() {
gpio_set_irq_enabled(VSYNC_PIN, GPIO_IRQ_EDGE_FALL | GPIO_IRQ_EDGE_RISE, true);
irq_set_exclusive_handler(IO_IRQ_BANK0, vga_board_button_irq_handler);
irq_set_enabled(IO_IRQ_BANK0, true);
}
uint32_t vga_board_get_buttons() {
return button_state;
}
#endif
struct text_element {
const char *text;
uint16_t color;
int width;
};
struct movie {
struct text_element text;
uint32_t start_sector;
uint32_t current_sector;
} *movies;
uint movie_count;
#define NAME_COLOR PICO_SCANVIDEO_PIXEL_FROM_RGB5(0x10, 0x10, 0x10)
struct movie static_movies[] = {
{ .text = {"<unknown name>", NAME_COLOR, 0} , .start_sector = 0 },
};
#define INSTR_COLOR1 PICO_SCANVIDEO_PIXEL_FROM_RGB5(0x10, 0x08, 0x08)
#define INSTR_COLOR2 INSTR_COLOR1
//0x4110
struct text_element instructions[] = {
{ "SPACE - toggle play/pause", INSTR_COLOR1 },
{ "ENTER - toggle display", INSTR_COLOR2 },
{ "'0' -> '4' - stopped -> fast", INSTR_COLOR1 },
{ "'+' / '-' - faster / slower(-mo)", INSTR_COLOR2 },
{ "'.' / ',' - step + / - one frame", INSTR_COLOR1 },
{ "'r' - reverse play direction!", INSTR_COLOR2 },
{ "'n' / 'p' - next / previous movie", INSTR_COLOR1 },
{ "'[' / ']' - down / up volume", INSTR_COLOR2 },
};
uint current_instruction;
uint32_t display_base_frame;
uint current_movie;//n = 1;
uint registered_current_movie; // as seen by decode
static struct font *font12;
static struct font *font18;
// to make sure only one core updates the state when the frame number changes
// todo note we should actually make sure here that the other core isn't still rendering (i.e. all must arrive before either can proceed - a la barrier)
static struct mutex frame_logic_mutex;
void init_render_state(int core);
static uint8_t playback_forwards = 1;
static int8_t playback_speed;
static int8_t hold_frame_count;
static int remaining_hold_frames = 1;
static int32_t next_frame_sector_override = -1;
static int32_t audio_sector_pairs_to_post_process;
static int32_t volume = 0x100;
static uint32_t total_audio_sectors;
static bool show_menu = false;
static int text_roller = 0;
struct font {
uint width_words;
uint height;
uint glyph_words; // == width_words * height;
uint32_t *pixels;
};
struct font *build_font(const lv_font_t *font, uint width_words) {
struct font *f = (struct font *)calloc(1, sizeof(struct font));
f->width_words = width_words;
f->height = font->line_height;
uint16_t colors[16];
for(int i=0;i<count_of(colors);i++) {
#ifdef PLATYPUS_565
colors[i] = 0x41 * i + 0x800 * (i/2);
#else
colors[i] = 0x21 * i + 0x400 * (i/2);
#endif
}
uint font_size_words = font->line_height * width_words;
f->pixels = (uint32_t *)calloc(4, font->dsc->cmaps->range_length * font_size_words);
f->glyph_words = f->width_words * f->height;
uint32_t *p = f->pixels;
for(int c=0; c< font->dsc->cmaps->range_length; c++) {
// inefficient but simple
assert(p == f->pixels + c * f->glyph_words);
const lv_font_fmt_txt_glyph_dsc_t *g = &font->dsc->glyph_dsc[c + 1];
const uint8_t *b = font->dsc->glyph_bitmap + g->bitmap_index;
int bi = 0;
for(int y = 0; y < f->height; y++) {
int ey = y - f->height + font->base_line + g->ofs_y + g->box_h;
for(int x = 0; x< f->width_words * 2; x++) {
uint32_t pixel;
int ex = x - g->ofs_x;
if (ex >=0 && ex < g->box_w && ey >= 0 && ey < g->box_h) {
pixel = bi&1 ? colors[b[bi>>1]&0xf] : colors[b[bi>>1]>>4];
bi++;
} else {
pixel = 0;
}
if (!(x & 1)) {
*p = pixel;
} else {
*p++ |= pixel<<16;
}
}
if (ey >= 0 && ey < g->box_h) {
for(int x = f->width_words * 2 - g->ofs_x; x < g->box_w; x++) {
bi++;
}
}
}
}
// printf("%p %p\n", p, font_raw_pixels + font->dsc->cmaps->range_length * font_size_words);
return f;
}
static void __popcorn_critical __attribute__((noinline)) reverse_sector_pair(uint32_t *s1, uint32_t *s2) {
for(int i=0;i<128;i++) {
uint32_t tmp = s1[i];
s1[i] = s2[127-i];
s2[127-i] = tmp;
}
}
uint16_t *draw_line(uint16_t *buf, uint16_t color, uint len) {
switch (len) {
case 0:
break;
case 1:
*buf++ = COMPOSABLE_RAW_1P;
*buf++ = color;
break;
case 2:
*buf++ = COMPOSABLE_RAW_2P;
*buf++ = color;
*buf++ = color;
break;
default:
*buf++ = COMPOSABLE_COLOR_RUN;
*buf++ = color;
*buf++ = (len - 3);
break;
}
return buf;
}
uint16_t bar_color(uint a, uint b) {
int x = a / 0x1000;
if (x > 0x1f) x = 0x1f;
return 0xc000 | x | ((0x1f - x) << 5);
}
int approx_log2(uint32_t x) {
int sig = 32 - __builtin_clz(x);
if (sig < 7) {
return (sig * 64) + ((x << (7 - sig))&63);
} else {
return (sig * 64) + ((x >> (sig - 7))&63);
}
}
uint16_t *draw_bar(uint16_t *buf, uint32_t color, uint32_t value, uint32_t maxv, uint bar_width) {
if (value > maxv) value = maxv;
uint w = (value * bar_width + maxv - 1) / maxv;
buf = draw_line(buf, color, w);
buf = draw_line(buf, 0x8842, bar_width -w);
buf = draw_line(buf, 0xc210, 4);
return buf;
}
#if 1
#define IMAGE_DATA_K 126
#else
#define IMAGE_DATA_K 116
#endif
#define AUDIO_BUFFER_K 6
#define IMAGE_DATA_WORDS (IMAGE_DATA_K * 256)
#define AUDIO
// todo see where we write off the end of this (hence need for + 128)
uint32_t __attribute__((section(".data._0"))) image_data[128 + IMAGE_DATA_K*1024 / 4] = {1}; // force into data not BSS
#define NUM_AUDIO_BUFFERS 2
uint32_t __attribute__((section(".data._5"))) audio_buffer[AUDIO_BUFFER_K*NUM_AUDIO_BUFFERS*1024 / 4] = {1};
uint32_t *const audio_buffer_start[NUM_AUDIO_BUFFERS] = {
audio_buffer,
audio_buffer + AUDIO_BUFFER_K * 256,
#if NUM_AUDIO_BUFFERS > 2
audio_buffer + AUDIO_BUFFER_K * 512
#endif
};
// font stolen from khan
#define __maybe_in_ram
extern const uint8_t atlantis_glyph_bitmap[];
extern const uint8_t atlantis_glyph_widths[];
#define menu_glypth_bitmap atlantis_glyph_bitmap
#define menu_glyph_widths atlantis_glyph_widths
#define MENU_GLYPH_MIN 32
#define MENU_GLYPH_COUNT 95
#define MENU_GLYPH_MAX (MENU_GLYPH_MIN + MENU_GLYPH_COUNT - 1)
#define MENU_GLYPH_HEIGHT 9
#define MENU_GLYPH_Y_OFFSET 2
#define MENU_GLYPH_ADVANCE 1
#define OVERLAY_WIDTH 80
#define OVERLAY_HEIGHT (17 + MENU_GLYPH_HEIGHT)
#define OVERLAY_X ((160 - OVERLAY_WIDTH) / 2)
#define OVERLAY_END 110
#define OVERLAY_START (OVERLAY_END - OVERLAY_HEIGHT)
static uint32_t overlay[OVERLAY_WIDTH * OVERLAY_HEIGHT * 2];
struct audio_buffer *audio_buffers[NUM_AUDIO_BUFFERS];
struct audio_buffer_pool *audio_buffer_pool;
#define MOVIE_ROWS 120
// +1 so we can tell full from empty
#define ROW_OFFSET_CIRCLE_SIZE (MOVIE_ROWS * 2 + 1 + 10)
static uint16_t row_buffer_offsets[ROW_OFFSET_CIRCLE_SIZE];
static uint row_wrap_add(uint a, uint b)
{
assert(a < ROW_OFFSET_CIRCLE_SIZE && b <= MOVIE_ROWS);
a += b;
if (a >= ROW_OFFSET_CIRCLE_SIZE) a -= ROW_OFFSET_CIRCLE_SIZE;
return a;
}
static uint row_wrap_sub(uint a, uint b)
{
assert(a < ROW_OFFSET_CIRCLE_SIZE && b<ROW_OFFSET_CIRCLE_SIZE);
a += ROW_OFFSET_CIRCLE_SIZE - b;
if (a >= ROW_OFFSET_CIRCLE_SIZE) a -= ROW_OFFSET_CIRCLE_SIZE;
return a;
}
#ifdef ENABLE_STRICT_ASSERTIONS
uint16_t ram_buffer_owning_row[RAM_BUFFER_K*1024 / 4] = {1};
#endif
static inline void set_owning_row(__unused uint from, __unused uint count, __unused uint16_t row)
{
#ifdef ENABLE_STRICT_ASSERTIONS
static uint16_t last_owning_row;
if (row != (uint16_t)-1)
{
assert(row == last_owning_row || row == row_wrap_add(last_owning_row, 1));
last_owning_row = row;
}
popcorn_debug("set owning row %04x->%04x %d\n", from, from+count, row);
for(int x = 0; x < count; x++)
{
// todo mark as unread once we track read amounts better.
ram_buffer_owning_row[from + x] = row; // 0x4000|row; // we mark this way as unread yet
}
#endif
}
//#define OUR_MAX_BLOCK_COUNT 16
//static_assert(MAX_BLOCK_COUNT >= OUR_MAX_BLOCK_COUNT, "");
//#undef MAX_BLOCK_COUNT
//#define MAX_BLOCK_COUNT OUR_MAX_BLOCK_COUNT
// + 1 for null end marker, and +1 for split buffer during wrap
static uint32_t scatter[(PICO_SD_MAX_BLOCK_COUNT + 2) * 4];
static uint32_t frame_header_sector[128];
struct frame_header {
uint32_t mark0;
uint32_t mark1;
uint32_t magic;
uint8_t major, minior, debug, spare;
uint32_t sector_number; // relative to start of stream
uint32_t frame_number;
uint8_t hh, mm, ss, ff; // good old CD days (bcd)
uint32_t header_words;
uint16_t width;
uint16_t height;
uint32_t image_words;
uint32_t audio_words; // just to confirm really
uint32_t audio_freq;
uint8_t audio_channels; // always assume 16 bit
uint8_t pad[3];
uint32_t unused[4]; // little space for expansion
uint32_t total_sectors;
uint32_t last_sector;
// 1, 2, 4, 8 frame increments
uint32_t forward_frame_sector[4];
uint32_t backward_frame_sectors[4];
// h/2 + 1 row_offsets, last one should == image_words
uint16_t row_offsets[];
} __attribute__((packed));
static struct decoder_state_state {
enum {
INIT,
NEW_FRAME,
NEED_FRAME_HEADER_SECTOR,
READING_FRAME_HEADER_SECTOR,
NEED_VIDEO_SECTORS,
PREPPING_VIDEO_SECTORS,
READING_VIDEO_SECTORS,
AWAIT_AUDIO_BUFFER,
NEED_AUDIO_SECTORS,
READING_AUDIO_SECTORS,
AUDIO_BUFFER_READY,
POST_PROCESSING_AUDIO_SECTORS,
ERROR,
FRAME_READY,
HIT_END,
} state;
struct {
uint32_t sector_base;
uint16_t frame_base_row;
uint16_t write_buffer_offset;
uint16_t remaining_row_words;
// todo we should combine these
uint16_t frame_row_count;
uint16_t row_index;
} video_read, video_read_rollback;
struct {
uint32_t sector_base;
uint16_t sector_count;
} current_sd_read;
struct {
uint16_t valid_from_row;
uint16_t valid_to_row;
// we display from frame_start to valid_to_row, and then wrap back prior to display_start (i.e.
// remaining data from previous frame in a pinch)...
// todo right now we always try and keep MOVIE_ROWS valid lines ending at valid_to_row
uint16_t display_start_row;
} rows;
struct {
volatile enum {
BS_EMPTY, BS_FILLING, BS_QUEUED
} buffer_state[2];
// these are nominally ! each other, however they might be slightly out of phase due to threading
// (i.e. they are owned by different threads)
// uint playback_buffer_index;
uint load_thread_buffer_index;
uint32_t sector_base;
} audio;
bool hold_frame;
bool paused;
uint8_t unpause;
bool awaiting_first_frame;
uint32_t display_time_code;
bool loaded_audio_this_frame;
} ds;
uint32_t last_display_time_code = -1;
static uint32_t waste[128]; // todo can this be ROM?
void popcorn_producer_pool_give_buffer(struct audio_connection *connection, struct audio_buffer *buffer)
{
// hand directly to consumer
queue_full_audio_buffer(connection->consumer_pool, buffer);
}
void popcorn_consumer_pool_give_buffer(struct audio_connection *connection, struct audio_buffer *buffer)
{
for(int i=0;i<NUM_AUDIO_BUFFERS;i++) {
if (buffer == audio_buffers[i]) {
DEBUG_PINS_CLR(audio_buffering, i + 1);
ds.audio.buffer_state[i] = BS_EMPTY;
return;
}
}
//panic("Unexpected buffer returned");
__breakpoint();
__builtin_unreachable();
}
static struct audio_connection popcorn_passthru_connection = {
.consumer_pool_take = consumer_pool_take_buffer_default,
.consumer_pool_give = popcorn_consumer_pool_give_buffer,
.producer_pool_take = producer_pool_take_buffer_default,
.producer_pool_give = popcorn_producer_pool_give_buffer,
};
#define PLATYPUS_MAGIC (('T'<<24)|('A'<<16)|('L'<<8)|'P')
static inline bool row_index_in_range(uint index, uint from, uint to) {
if (from <= to) {
return index >= from && index < to;
} else {
return index >= from || index < to;
}
}
static uint peek_upcoming_row(struct frame_header *head, int ahead) {
if (ds.video_read.frame_row_count + ahead >= MOVIE_ROWS) {
return 0;
}
return head->row_offsets[ds.video_read.frame_row_count + ahead + 1] - head->row_offsets[ds.video_read.frame_row_count + ahead];
}
struct gpt_entry {
uint64_t ptype1, ptype2;
uint64_t guid1, guid2;
uint64_t first_lba;
uint64_t last_lba;
uint64_t attributes;
uint16_t u_name[36];
};
#pragma GCC push_options
#pragma GCC optimize("O3")
static void __attribute__((noinline)) update_audio_sector_volume(uint32_t *_samples) {
int16_t *samples = (int16_t *)_samples;
for(int i=0;i<256;i++) {
samples[i] = (samples[i] * volume) >> 8;
}
}
#pragma GCC pop_options
static __popcorn_critical __attribute__((noinline)) void sd_state_update() {
struct frame_header *head = (struct frame_header *)frame_header_sector;
switch (ds.state) {
case NEW_FRAME: {
ds.state = NEED_FRAME_HEADER_SECTOR;
ds.loaded_audio_this_frame = false;
break;
}
case NEED_FRAME_HEADER_SECTOR:
{
head->mark0 = 0; // mark as invalid
// printf("starting scatter read %d\n", (uint)vs.sector_num);
const int sector_count = 1;
assert(sector_count <= PICO_SD_MAX_BLOCK_COUNT);
uint32_t *p = scatter;
for(int i = 0; i < sector_count; i++)
{
*p++ = native_safe_hw_ptr((frame_header_sector + i * 128));
*p++ = 128;
// for now we read the CRCs also
*p++ = native_safe_hw_ptr(waste);
*p++ = 2;
}
*p++ = 0;
*p++ = 0;
ds.current_sd_read.sector_count = sector_count;
sd_readblocks_scatter_async(scatter, ds.current_sd_read.sector_base, ds.current_sd_read.sector_count);
ds.state = READING_FRAME_HEADER_SECTOR;
break;
}
case READING_FRAME_HEADER_SECTOR:
{
if (sd_scatter_read_complete(NULL))
{
struct frame_header *head = (struct frame_header *) frame_header_sector;
if (head->mark0 != 0xffffffff || head->mark1 != 0xffffffff || head->magic != PLATYPUS_MAGIC)
{
printf("No header found @ %d\n", (int) ds.current_sd_read.sector_base);
// ds.state = NONE;
#if 1
ds.current_sd_read.sector_base++;
ds.state = NEED_FRAME_HEADER_SECTOR;
#else
panic("doh");
ds.state = ERROR;
#endif
}
else
{
//printf("Found frame header @ %08x %02x:%02x:%02x.%02x\n", (uint)vs.sector_num, head->hh, head->mm, head->ss, head->ff);
assert(ds.current_sd_read.sector_count == 1);
if (head->header_words > 128)
{
printf("expect 1 sector header\n");
ds.current_sd_read.sector_base++;
ds.state = NEED_FRAME_HEADER_SECTOR;
} else
{
movies[registered_current_movie].current_sector = ds.current_sd_read.sector_base;
ds.display_time_code = (head->hh << 24u) | (head->mm << 16u) | (head->ss << 8u) | (head->ff);
ds.current_sd_read.sector_base++;
// skip audio sectors
ds.audio.sector_base = ds.current_sd_read.sector_base;
ds.current_sd_read.sector_base += (head->audio_words + 127) / 128;
ds.video_read.sector_base = ds.current_sd_read.sector_base;
ds.video_read.frame_base_row = ds.rows.valid_to_row;
ds.video_read.frame_row_count = 0;
ds.state = NEED_VIDEO_SECTORS;
}
}
}
break;
}
case READING_VIDEO_SECTORS:
{
assert(ds.current_sd_read.sector_count);
if (sd_scatter_read_complete(NULL))
{
// todo arguably we can track the read in progress to update rows as they become available
uint row_count = ds.video_read.frame_row_count;
// check for incomplete row
if (ds.video_read.remaining_row_words)
{
assert(row_count);
// todo is this correct.... seems like we have a partial row visible, or maybe a partial row invisible which is fine
popcorn_debug("acknowledge %d words from previous\n", ds.video_read.remaining_row_words);
row_count--;
}
uint __unused was = ds.rows.valid_to_row;
ds.rows.valid_to_row = row_wrap_add(ds.video_read.frame_base_row, row_count);
popcorn_debug("completed read %d->%d\n", was, ds.rows.valid_to_row);
#ifdef JUST_READ
uint trail = row_wrap_sub(ds.rows.valid_to_row, MOVIE_ROWS);
if (row_index_in_range(trail, ds.rows.valid_from_row, ds.rows.valid_to_row)) {
popcorn_debug("--- truncating valid from ---\n");
ds.rows.valid_from_row = trail;
}
#endif
ds.video_read.sector_base += ds.current_sd_read.sector_count;
ds.current_sd_read.sector_base = ds.video_read.sector_base;
ds.current_sd_read.sector_count = 0;
ds.state = NEED_VIDEO_SECTORS;
}
break;
}
case INIT:
{
movie_count = 1;
movies = static_movies;
if (sd_init_4pins() < 0)
{
ds.state = ERROR;
} else {
#ifdef VGABOARD_BUTTON_A_PIN
gpio_init(23);
gpio_set_dir(23, GPIO_OUT);
gpio_put(23, 1);
#endif
sd_readblocks_sync(image_data, 1, 1);
const struct gpt_header {
uint64_t signature;
uint32_t revision;
uint32_t size;
uint32_t crc;
uint32_t _pad;
uint64_t lba;
uint64_t backup_lba;
uint64_t first_usable_lba;
uint64_t last_usabble_lba;
uint64_t guid1, guid2;
uint64_t table_lba;
uint32_t table_count;
uint32_t table_entry_size;
uint32_t table_crc;
} __packed gpt_header = *(const struct gpt_header *)image_data;
// todo crc
if (gpt_header.signature == 0x5452415020494645ULL && gpt_header.size == sizeof(struct gpt_header)) {
printf("Found GPT\n");
int sectors = (gpt_header.table_count * gpt_header.table_entry_size + 511) / 512;
int read_sectors = MAX(sectors, 32);
printf(" reading %d/%d sectors starting at %d\n", read_sectors, sectors, (int)gpt_header.table_lba);
sd_readblocks_sync(image_data, gpt_header.table_lba, read_sectors);
uint8_t *buffer = (uint8_t *)image_data;
movie_count = 0;
for(uint i=0;i<gpt_header.table_count;i++) {
struct gpt_entry *gpt_entry = (struct gpt_entry *)(buffer + i * gpt_header.table_entry_size);
if (gpt_entry->ptype1 || gpt_entry->ptype2) {
sd_readblocks_sync(frame_header_sector, gpt_entry->first_lba, 1);
if (head->mark0 == 0xffffffff && head->mark1 == 0xffffffff || head->magic == PLATYPUS_MAGIC) {
movie_count++;
} else {
gpt_entry->ptype1 = gpt_entry->ptype2 = 0;
}
}
}
if (!movie_count) {
panic("No movies found");
}
movies = (struct movie *)calloc(movie_count, sizeof(struct movie));
for(uint i=0,j=0;i<gpt_header.table_count;i++) {
struct gpt_entry *gpt_entry = (struct gpt_entry *)(buffer + i * gpt_header.table_entry_size);
if (gpt_entry->ptype1 || gpt_entry->ptype2) {
movies[j].start_sector = gpt_entry->first_lba;
movies[j].text.color = NAME_COLOR;
const int MAX_TEXT = 20; // random ... must be less than 36
char *ascii = (char*)gpt_entry->u_name;
for(int k=0;k<MAX_TEXT;k++) {
char c = gpt_entry->u_name[k];
ascii[k] = c; // overwrite the string at half speed
if (!c) break;
}
ascii[MAX_TEXT] = 0;
movies[j].text.text = strdup(ascii);
printf("'%s' at %08x\n", movies[j].text.text, (uint)movies[j].start_sector);
j++;
}
}
} else {
printf("No GPT found, so assuming single movie\n");
}
}
ds.audio.buffer_state[0] = ds.audio.buffer_state[1] = BS_EMPTY;
ds.audio.load_thread_buffer_index = 0;
ds.rows.valid_from_row = ds.rows.valid_to_row = 0;
ds.rows.display_start_row = 0;//row_wrap_sub(0, MOVIE_ROWS);
ds.awaiting_first_frame = 1;
ds.hold_frame = true;
registered_current_movie = current_movie;
ds.current_sd_read.sector_base = movies[current_movie].start_sector;
ds.state = NEW_FRAME;
break;
}
case ERROR:
{
panic("doh");
break;
}
case AWAIT_AUDIO_BUFFER:
{
if (ds.audio.buffer_state[ds.audio.load_thread_buffer_index] == BS_EMPTY)
{
ds.audio.buffer_state[ds.audio.load_thread_buffer_index] = BS_FILLING;
ds.state = NEED_AUDIO_SECTORS;
}
break;
}
case HIT_END:
{
if (ds.hold_frame && ds.audio.buffer_state[ds.audio.load_thread_buffer_index] == BS_EMPTY && !ds.loaded_audio_this_frame)
{
ds.audio.buffer_state[ds.audio.load_thread_buffer_index] = BS_FILLING;
ds.state = NEED_AUDIO_SECTORS;
}
else
{
ds.state = NEED_VIDEO_SECTORS;
}
break;
}
case READING_AUDIO_SECTORS:
{
if (sd_scatter_read_complete(NULL))
{
// todo we have DMA completely capable of reading backwards - seems like a strange thing to expose in any lower level API
// still we could use DMA to reverse the buffers for us (although it is a bit complicated to not step on our toes)
if (!playback_forwards || volume != 0x100) {
total_audio_sectors = (head->audio_words + 127) / 128;
if (total_audio_sectors & 1) {
panic("e");//xpect even audio sector count for now");
}
audio_sector_pairs_to_post_process = total_audio_sectors / 2; // we do them in pairs
ds.state = POST_PROCESSING_AUDIO_SECTORS;
} else
{
ds.state = AUDIO_BUFFER_READY;
}
}
break;
}
case POST_PROCESSING_AUDIO_SECTORS:
{
assert(audio_sector_pairs_to_post_process > 0);
audio_sector_pairs_to_post_process--;
if (!playback_forwards)
{
reverse_sector_pair(audio_buffer_start[ds.audio.load_thread_buffer_index] +
128 * audio_sector_pairs_to_post_process,
audio_buffer_start[ds.audio.load_thread_buffer_index] +
128 * (total_audio_sectors - 1 - audio_sector_pairs_to_post_process));
}
if (volume != 0x100) {
update_audio_sector_volume(audio_buffer_start[ds.audio.load_thread_buffer_index] +
128 * audio_sector_pairs_to_post_process);
update_audio_sector_volume(audio_buffer_start[ds.audio.load_thread_buffer_index] +
128 * (total_audio_sectors - 1 - audio_sector_pairs_to_post_process));
}
if (!audio_sector_pairs_to_post_process) {
ds.state = AUDIO_BUFFER_READY;
}
break;
}
case FRAME_READY:
{
// not much to do really now other than start reading data for next sector
ds.awaiting_first_frame = false;
uint index = playback_speed >= 0 ? MIN(playback_speed, 3) : 0;
if (next_frame_sector_override != -1)
{
ds.current_sd_read.sector_base = next_frame_sector_override;
next_frame_sector_override = -1;
registered_current_movie = current_movie;
} else {
uint32_t next_sector;
if (ds.paused)
{
next_sector = head->sector_number;
} else {
if (playback_forwards)
{
next_sector = head->forward_frame_sector[index];
}
else
{
next_sector = head->backward_frame_sectors[index];
}
}
if (next_sector == 0xffffffff) {
if (playback_forwards) {
next_sector = 0;
} else {
next_sector = head->last_sector;
}
}
ds.current_sd_read.sector_base = movies[registered_current_movie].start_sector + next_sector;
}
if (playback_speed < 0) hold_frame_count = 1 - playback_speed;
else hold_frame_count = 1;
ds.state = NEW_FRAME;
break;
}
case NEED_VIDEO_SECTORS:
{
//ds.video_read.frame_base_row = ds.rows.valid_to_row; // todo is this always correct; it assumes the last row always has the right scanline number (us minus 1)
ds.video_read_rollback = ds.video_read;
ds.state = PREPPING_VIDEO_SECTORS;
break;
}
case NEED_AUDIO_SECTORS:
{
DEBUG_PINS_SET(audio_buffering, 4);
assert(ds.audio.buffer_state[ds.audio.load_thread_buffer_index] == BS_FILLING);
audio_buffers[ds.audio.load_thread_buffer_index]->sample_count = head->audio_words;
// todo update sd.current_read_sector for consistency... can't do it until we pick the next frame sector explicitly rather than just happening into it.
sd_readblocks_async(audio_buffer_start[ds.audio.load_thread_buffer_index], ds.audio.sector_base,
(head->audio_words + 127) / 128);
ds.state = READING_AUDIO_SECTORS;
break;
}
case PREPPING_VIDEO_SECTORS:
case AUDIO_BUFFER_READY:
// handled below
break;
}
if (ds.state == PREPPING_VIDEO_SECTORS) {
bool done = false;
// we are making a decision about how many sectors we can read (we read up to MAX_BLOCK_COUNT - 1) in case one is split
uint32_t *p = scatter;
int sector_count = 0;
uint32_t part1_buffer_offset;
uint32_t part1_size;
// note part2 is always loaded at ram offset 0
uint32_t part2_size;
uint buffer_offset_limit = row_buffer_offsets[ds.rows.valid_from_row];
uint frame_row_count_limit = row_wrap_sub(ds.rows.valid_from_row, row_wrap_add(ds.video_read.frame_base_row, 1));
#ifdef DEBUG_UNKNOWN_ISSUE1
if (ds.video_read.frame_row_count > frame_row_count_limit) {
printf("Whurrt %d %d\n", ds.video_read.frame_row_count, frame_row_count_limit);
}
#endif
// constraint 1) we must have enough space for the scatter information (part1 + (part2) + CRC) * 2 = 6
while (sector_count < PICO_SD_MAX_BLOCK_COUNT && p < scatter + count_of(scatter) - 6)
{
uint words_until_wrap = IMAGE_DATA_WORDS - ds.video_read.write_buffer_offset;
bool may_wrap = buffer_offset_limit < ds.video_read.write_buffer_offset || ds.rows.valid_from_row == ds.rows.valid_to_row; // (latter is empty buffer)
uint linear_words = may_wrap ? words_until_wrap : buffer_offset_limit - ds.video_read.write_buffer_offset;
popcorn_debug("s %d r %d val %04x ->| %04x (%d %d %d)", (uint)ds.video_read.sector_base + sector_count, ds.video_read.frame_row_count,
ds.video_read.write_buffer_offset, buffer_offset_limit,
ds.rows.valid_from_row, ds.rows.display_start_row, ds.rows.valid_to_row);
// if (2783 == (uint)ds.video_read.sector_base + sector_count &&
// 109 == ds.video_read.frame_row_count &&
// 0x70f2 == ds.video_read.write_buffer_offset) {
// printf("Hello\n");
// }
if (may_wrap) {
popcorn_debug(" may-wrap @ %04x", IMAGE_DATA_WORDS);
}
popcorn_debug("\n");
// constraint 2) we must have space for a sector - conservatively we assume if we have to wrap we need space
// for a whole sector at the beginning of the buffer
if (linear_words < 128 && !may_wrap) {//} || buffer_offset_limit < 128)) {
popcorn_debug(" no room and can't wrap\n");
break;
}
uint row_index = ds.video_read.row_index;
if (!ds.video_read.remaining_row_words) {
// we need a new row
uint remaining_row_words = peek_upcoming_row(head, 0);
row_index = row_wrap_add(ds.video_read.frame_base_row, ds.video_read.frame_row_count);
if (!remaining_row_words) {
done = true;
break;
}
if (ds.video_read.frame_row_count == frame_row_count_limit) {
//printf("ooh out of rows %d %d %d %d+%d.%d\n", ds.rows.valid_from_row, ds.rows.display_start_row, ds.rows.valid_to_row, ds.video_read.frame_base_row, ds.video_read.frame_row_count, ds.video_read.remaining_row_words);
popcorn_debug(" would start new row on sector boundary but out of rows space\n");
break;
}
ds.video_read.remaining_row_words = remaining_row_words; // we can't update this prior
popcorn_debug(" start new row on sector boundary ri %d\n", row_index);
// we have a new row, so decide whether it goes here or after wrap
if (ds.video_read.remaining_row_words > words_until_wrap) {
if (ds.video_read.write_buffer_offset <= buffer_offset_limit) {
popcorn_debug(" too early to wrap\n");
break;
}
popcorn_debug(" moving entire new row to beginning\n");
ds.video_read.write_buffer_offset = 0;
words_until_wrap = linear_words = buffer_offset_limit;
may_wrap = false;
}
row_buffer_offsets[row_index] = ds.video_read.write_buffer_offset;
ds.video_read.frame_row_count++;
ds.video_read.row_index = row_index;
}
assert(ds.video_read.remaining_row_words); // there should indeed be row words then
if (ds.video_read.remaining_row_words >= 128) {
uint this_time_words = MIN(ds.video_read.remaining_row_words, 128);
if (linear_words < this_time_words) {
popcorn_debug(" not enough space yet\n");
break;
}
popcorn_debug(" 128 of %d\n", ds.video_read.remaining_row_words);
part1_buffer_offset = ds.video_read.write_buffer_offset;
part1_size = 128;
part2_size = 0;
set_owning_row(part1_buffer_offset, part1_size, row_index);
ds.video_read.remaining_row_words -= this_time_words;
ds.video_read.write_buffer_offset += 128;
} else {
// note that here we are in the middle of a row, therefore we cannot be splitting, which is why we care
// about linear_words
if (linear_words < ds.video_read.remaining_row_words) {
popcorn_debug(" not enough space yet even for remaining\n");
break;
}
part1_buffer_offset = ds.video_read.write_buffer_offset;
part1_size = ds.video_read.remaining_row_words;
part2_size = 0;
set_owning_row(part1_buffer_offset, part1_size, row_index);
uint consumed = ds.video_read.remaining_row_words;
uint saved_remaining_row_words = ds.video_read.remaining_row_words;
ds.video_read.write_buffer_offset += ds.video_read.remaining_row_words;
popcorn_debug(" %d remaining of ri %d, then ", ds.video_read.remaining_row_words, row_index);
ds.video_read.remaining_row_words = 0;
bool rollback = false;
int rows_ahead = 0;
while (consumed < 128 && !rollback) {
assert(!ds.video_read.remaining_row_words);
ds.video_read.remaining_row_words = peek_upcoming_row(head, rows_ahead);
if (!ds.video_read.remaining_row_words) {
if (part2_size) {
// printf("padding part 2\n");
part2_size += 128 - consumed;
} else {
// printf("padding part 1\n");
part1_size += 128 - consumed;
}
set_owning_row(ds.video_read.write_buffer_offset, 128 - consumed, -1);
popcorn_debug("padding");
done = true;
break; // end of frame
}
if ((ds.video_read.frame_row_count + rows_ahead) >= frame_row_count_limit) {
#ifdef DEBUG_UNKNOWN_ISSUE1
if (ds.video_read.frame_row_count + rows_ahead > frame_row_count_limit) {
printf("What %d %d %d\n", ds.video_read.frame_row_count, rows_ahead, frame_row_count_limit);
}
#endif
assert((ds.video_read.frame_row_count + rows_ahead) == frame_row_count_limit);
popcorn_debug(" out of rows space; rollback!\n");
rollback = true;
} else {
uint to_consume = MIN(128 - consumed, ds.video_read.remaining_row_words);
row_index = row_wrap_add(ds.video_read.frame_base_row, ds.video_read.frame_row_count + rows_ahead);
if (consumed + ds.video_read.remaining_row_words > words_until_wrap) {
//popcorn_debug("(%d %d %d %d %d)", consumed + ds.video_read.remaining_row_words, words_until_wrap, to_consume, ds.video_read.write_buffer_offset, buffer_offset_limit);
// this row will have to wrap anyway
if (!may_wrap || (ds.video_read.remaining_row_words >= buffer_offset_limit && buffer_offset_limit < 128)) {
popcorn_debug("not enough space to wrap .... rollback!");
rollback = true;
} else
{
assert(may_wrap);
popcorn_debug("wrap, and ");
assert(!part2_size);
part2_size += to_consume;
ds.video_read.write_buffer_offset = 0;
set_owning_row(ds.video_read.write_buffer_offset, to_consume, row_index);
words_until_wrap = linear_words = buffer_offset_limit;
may_wrap = false;
}
} else {
if (part2_size) {
// if we've already wrapped, then we need to add to that
part2_size += to_consume;
} else
{
part1_size += to_consume;
}
set_owning_row(ds.video_read.write_buffer_offset, to_consume, row_index);
}
if (!rollback)
{
popcorn_debug("%d from new row ri = %d(of %d)", to_consume, row_index,
(uint) ds.video_read.remaining_row_words);
row_buffer_offsets[row_index] = ds.video_read.write_buffer_offset;
ds.video_read.remaining_row_words -= to_consume;
ds.video_read.write_buffer_offset += to_consume;
consumed += to_consume;
rows_ahead++;
}
}
}
popcorn_debug("\n");
if (rollback) {
// we had to rollback
ds.video_read.remaining_row_words = saved_remaining_row_words;
ds.video_read.write_buffer_offset = part1_buffer_offset;
break;
}
// printf("wfr %d ra %d\n", vs.write_frame_row_count, rows_ahead);
ds.video_read.frame_row_count += rows_ahead;
ds.video_read.row_index = row_index;
}
// we must read a whole sector
assert(part1_size + part2_size == 128);
popcorn_debug(" read %d at %04x ", (uint)part1_size, (uint)part1_buffer_offset);
*p++ = native_safe_hw_ptr(image_data + part1_buffer_offset);
*p++ = part1_size;
if (part2_size)
{
popcorn_debug(" and %d at 0000", (uint)part2_size);
*p++ = native_safe_hw_ptr(image_data); // part2 always at start of buffer
*p++ = part2_size;
}
popcorn_debug("\n");
// CRC
*p++ = native_safe_hw_ptr(waste);
*p++ = 2;
sector_count++;
}
if (sector_count)
{
*p++ = 0;
*p++ = 0;
assert(p <= scatter + count_of(scatter));
ds.current_sd_read.sector_base = ds.video_read.sector_base;
ds.current_sd_read.sector_count = sector_count;
popcorn_debug("starting read %d secs @ %04x?(%04x) -> %04x(%04x)\n", sector_count, row_buffer_offsets[row_wrap_add(ds.video_read.frame_base_row, ds.video_read_rollback.frame_row_count)], ds.video_read_rollback.write_buffer_offset,
row_buffer_offsets[row_wrap_add(ds.video_read.frame_base_row, ds.video_read.frame_row_count-1)], ds.video_read.write_buffer_offset);
sd_readblocks_scatter_async(scatter, ds.video_read.sector_base, sector_count);
ds.state = READING_VIDEO_SECTORS;
} else if (done) {
if (!ds.loaded_audio_this_frame) {
ds.state = AWAIT_AUDIO_BUFFER;
} else
{
ds.state = FRAME_READY;
}
} else {
ds.state = HIT_END;
}
} else if (ds.state == AUDIO_BUFFER_READY) {
DEBUG_PINS_CLR(audio_buffering, 4);
ds.loaded_audio_this_frame = true;
ds.audio.buffer_state[ds.audio.load_thread_buffer_index] = BS_QUEUED;
if (!ds.paused && (playback_speed > -3 && playback_speed < 3))
{
if (playback_forwards)
{
audio_buffers[ds.audio.load_thread_buffer_index]->buffer->bytes = (uint8_t *)audio_buffer_start[ds.audio.load_thread_buffer_index];
} else {
// need to offset the audio because it is now right aligned
audio_buffers[ds.audio.load_thread_buffer_index]->buffer->bytes = (uint8_t *)(audio_buffer_start[ds.audio.load_thread_buffer_index] + (127u & -head->audio_words));
}
#if PICO_ON_DEVICE
DEBUG_PINS_SET(audio_buffering, ds.audio.load_thread_buffer_index + 1);
give_audio_buffer(audio_buffer_pool, audio_buffers[ds.audio.load_thread_buffer_index]);
#else
ds.audio.buffer_state[ds.audio.load_thread_buffer_index] = BS_EMPTY;
#endif
} else {
ds.audio.buffer_state[ds.audio.load_thread_buffer_index] = BS_EMPTY;
}
ds.audio.load_thread_buffer_index++;
if (ds.audio.load_thread_buffer_index == NUM_AUDIO_BUFFERS) ds.audio.load_thread_buffer_index = 0;
ds.state = NEED_VIDEO_SECTORS;
}
}
#ifdef PLATYPUS_565
#define DARKEN_MASK 0x7bcf7bcf
#else
#define DARKEN_MASK 0x3def3def
#endif
static inline void darken(uint32_t *p, uint32_t row_delta, uint32_t *o, int32_t c) {
int i=0;
// static int foobar;
// foobar++;
// uint32_t offset = ((foobar >> 11)&0xf)*0x4210421;
do {
p[i] = ((p[i]>>1)&DARKEN_MASK)+o[i];
p[row_delta+i] = ((p[row_delta + i]>>1)&DARKEN_MASK)+o[i+OVERLAY_WIDTH];
i++;
} while (i < c);
}
#if PICO_ON_DEVICE
void __attribute__((optimize("Os"))) __no_inline_not_in_flash_func(darken_a)(uint32_t *p, uint32_t row_delta, uint32_t *o, int32_t c) {
darken(p, row_delta, o, c);
}
void __attribute__((optimize("Os"))) __no_inline_not_in_flash_func(darken_b)(uint32_t *p, uint32_t row_delta, uint32_t *o, int32_t c) {
darken(p, row_delta, o, c);
}
#else
#define darken_a darken
#define darken_b darken
#endif
void check_debug(const uint32_t *end, struct frame_header *header, uint row) {
// hmmm... we still see this on device, but nothing on host... don't see any visual distortion
if (false && header->debug)
{
uint8_t *b = (uint8_t *)end;
if (b[0] != 0xaa || b[3] != row) {
b -= 4; // we might have overshot todo fix decompress_row to always return the right value
if (b[0] != 0xaa || b[3] != row) {
b += 8; // we might have overshot todo fix decompress_row to always return the right value
if (b[0] != 0xaa || b[3] != row)
{
printf("%p %02x %02x\n", b, b[0], b[1]);
printf("Bad row data %08x hf %d row %d\n", (uint) *end, (int) header->frame_number, row);
printf("%p %04x\n", end, ((uint32_t *) b) - image_data);
printf("\n");
}
}
}
}
}
void set_unpause() {
if (ds.paused) {
ds.unpause = 3; // take care of buffered (overkill)
}
}
void draw_glyph(struct font *font, uint32_t *dest, uint32_t c)
{
uint32_t *src = font->pixels + c * font->glyph_words;
for(int y=0;y<font->height;y++) {
for(int x=0; x<font->width_words;x++) {
dest[x] = src[x];
}
dest += OVERLAY_WIDTH;
src += font->width_words;
}
}
uint render_font_line(const struct text_element *element, uint16_t *out, const uint8_t *bitmaps, uint16_t color) {
const char *p = element->text;
uint32_t acc = 0;
uint8_t c;
int bits = 0;
uint16_t *o = out;
while ((c = *p++)) {
c -= MENU_GLYPH_MIN;
if (c < MENU_GLYPH_MAX) {
int cbits = menu_glyph_widths[c] + 1;
if (cbits <= 8) {
acc = (acc << cbits) | ((bitmaps[c * MENU_GLYPH_HEIGHT] >> (8 - cbits)));
} else {
acc = (acc << cbits) | ((bitmaps[c * MENU_GLYPH_HEIGHT] << (cbits - 8)));
}
bits += cbits;
if (bits >= 24) {
while (--bits > 0) {
*o++ = ((acc >> bits)&1u) ? color : 0;
}
acc = 0;
}
}
}
while (--bits > 0) {
*o++ = ((acc >> bits)&1u) ? color : 0;
}
return o - out;
}
void __maybe_in_ram measure_text(struct text_element *element, int max) {
if (!element->width)
{
element->width = 0;
if (element->text)
{
const char *p = element->text;
uint8_t c;
while ((c = *p++))
{
c -= MENU_GLYPH_MIN;
if (c < MENU_GLYPH_MAX)
{
element->width += menu_glyph_widths[c] + MENU_GLYPH_ADVANCE;
}
}
if (element->width > max)
{
panic("too long");
element->width = max;
}
}
}
}
void __popcorn_critical render_loop() {
static uint8_t last_input = 0;
static volatile int32_t last_scanline_id[2];
static uint32_t last_frame_num[2] = {-1, -1};
static uint32_t core_1_last_frame_num = -1;
static int which = 0;
uint core_num = get_core_num();
assert(core_num >=0 && core_num < 2);
printf("Rendering on core %d\r\n", core_num);
while (true) {
struct scanvideo_scanline_buffer *sb[2];
sb[0] = scanvideo_begin_scanline_generation_linked(2, true);
sb[0]->link_after = 2;
sb[1] = sb[0]->link;
struct scanvideo_scanline_buffer *scanline_buffer = sb[0];
uint this_display_start_row;
// do any frame related logic
mutex_enter_blocking(&frame_logic_mutex);
uint frame_num = scanvideo_frame_number(scanline_buffer->scanline_id);
bool core_0_beat_core_1_to_new_frame = false;
// need to do this for whichever core got here first
// note that with multiple cores we may have got here not for the first scanline, however one of the cores will do this logic first before either does the actual generation
if (frame_num != last_frame_num[core_num])
{
#ifdef SHOW_PERF_COUNTERS
if (!core_num)
{
static uint32_t *val_ptr = perf_count;
static int rotator = 0;
for(int i = 0; i < 4; i++)
{
val_ptr[i] = bus_ctrl_hw->counter[i].value;
bus_ctrl_hw->counter[i].value = 0;
}
if (rotator & 1)
{
if (rotator & 2)
{
bus_ctrl_hw->counter[0].sel = arbiter_sram0_perf_event_access_contested;
bus_ctrl_hw->counter[1].sel = arbiter_sram1_perf_event_access_contested;
bus_ctrl_hw->counter[2].sel = arbiter_sram2_perf_event_access_contested;
bus_ctrl_hw->counter[3].sel = arbiter_sram3_perf_event_access_contested;
val_ptr = perf_contention;
}
else
{
bus_ctrl_hw->counter[0].sel = arbiter_sram0_perf_event_access;
bus_ctrl_hw->counter[1].sel = arbiter_sram1_perf_event_access;
bus_ctrl_hw->counter[2].sel = arbiter_sram2_perf_event_access;
bus_ctrl_hw->counter[3].sel = arbiter_sram3_perf_event_access;
val_ptr = perf_count;
}
}
else
{
if (rotator & 2)
{
bus_ctrl_hw->counter[0].sel = arbiter_rom_perf_event_access_contested;
bus_ctrl_hw->counter[1].sel = arbiter_xip_main_perf_event_access_contested;
bus_ctrl_hw->counter[2].sel = arbiter_apb_perf_event_access_contested;
bus_ctrl_hw->counter[3].sel = arbiter_fastperi_perf_event_access_contested;
val_ptr = perf_contention + 4;
}
else
{
bus_ctrl_hw->counter[0].sel = arbiter_rom_perf_event_access;
bus_ctrl_hw->counter[1].sel = arbiter_xip_main_perf_event_access;
bus_ctrl_hw->counter[2].sel = arbiter_apb_perf_event_access;
bus_ctrl_hw->counter[3].sel = arbiter_fastperi_perf_event_access;
val_ptr = perf_count + 4;
}
}
for(int i = 0; i < 4; i++)
{
bus_ctrl_hw->counter[i].value = 0;
}
rotator++;
}
#define MAXX 0xc0000
for(int qq = 0; qq < 2 ; qq++) {
int total = 0;
uint16_t bar_width = vga_mode.width / 9 - 4;
uint16_t *buf_base = bar_chart[core_num][qq];
uint16_t *buf = buf_base;
for (int i = 0; i < 8; i++) {
if (qq) {
total += perf_count[i];
buf = draw_bar(buf, 0xfd08, perf_count[i], MAXX, bar_width);
} else {
total += perf_contention[i];
buf = draw_bar(buf, bar_color(perf_contention[i], perf_count[i]), perf_contention[i] , perf_count[i], bar_width);
}
};
buf = draw_bar(buf, qq? 0xfd08: bar_color(total, MAXX), total, MAXX, bar_width);
*buf++ = COMPOSABLE_RAW_1P;
*buf++ = 0;
if (2 & (uintptr_t) buf) {
*buf++ = COMPOSABLE_EOL_ALIGN;
} else {
*buf++ = COMPOSABLE_EOL_SKIP_ALIGN;
*buf++ = 0xffff;
}
bar_chart_size[core_num][qq] = (buf - buf_base) / 2;
}
// printf("\r%08x %04x %08x %08x %08x %08x %08x %08x %08x : %08x", perf_count[0], approx_log2(perf_count[0]), perf_count[1], perf_count[2], perf_count[3], perf_count[4], perf_count[5], perf_count[6], perf_count[7], total);
// printf("\r%08x %08x %08x %08x %08x %08x", perf_contention[2], perf_contention[3], perf_contention[4], perf_contention[5], perf_contention[6], perf_contention[7]);
#endif
// this could should be during vblank as we try to create the next line
// todo should we ignore if we aren't attempting the next line
last_frame_num[core_num] = frame_num;
if (!core_num)
{
#if 0
// todo default button
uint8_t new_input = gpio_get(28);
if (last_input && !new_input)
{
//left++;
//printf("%d\n", left);
show_menu = !show_menu;
display_base_frame = scanvideo_frame_number(scanvideo_get_next_scanline_id());
}
last_input = new_input;
#endif
if (show_menu) {
if (ds.display_time_code != last_display_time_code) {
uint32_t c = ds.display_time_code;
uint32_t *o = overlay + OVERLAY_WIDTH * 2 + 18;
for(int n=0;n<6;n++) {
draw_glyph(font18, o, (c>>28)+1);
c = c<<4;
o += font18->width_words;
if (n == 1 || n==3) {
draw_glyph(font18, o, 11); // :
o += 2;
} else if (n == 5) {
draw_glyph(font18, o, 0); // .
o += 2;
}
}
o += (lcd18.line_height - lcd12.line_height) * OVERLAY_WIDTH;
for(int n=0;n<2;n++) {
draw_glyph(font12, o, (c>>28)+1);
c = c<<4;
o += font12->width_words;
}
last_display_time_code = ds.display_time_code;
}
text_roller = 0;
}
if (frame_num != core_1_last_frame_num) {
core_0_beat_core_1_to_new_frame = true;
}
}
}
bool this_hold_frame = ds.hold_frame;
if (core_num) {
if (frame_num != core_1_last_frame_num)
{
core_1_last_frame_num = frame_num;
#ifdef VGABOARD_BUTTON_A_PIN
if (button_state != last_button_state) {
for(uint b=0;b<2;b++) {
}
}
#endif
if (uart_is_readable(uart_default))
{
char c = uart_getc(uart_default);
uint old_movie = current_movie;
if (c>='0' && c<='9') {
if (c== '0')
{
ds.paused = true;
} else if (c== '1') {
playback_speed = 0;
ds.paused = false;
} else if (c== '2') {
playback_speed = 1;
ds.paused = false;
} else if (c== '3') {
playback_speed = 2;
ds.paused = false;
} else if (c== '4')
{
playback_speed = 3;
ds.paused = false;
}
} else if (c == 'x') {
static bool off;
off = !off;
int func =off ? GPIO_FUNC_SIO : GPIO_FUNC_PIO0;
gpio_set_function(0, func);
gpio_set_function(6, func);
gpio_set_function(11, func);
} else if (c == 'r') {
playback_forwards ^= 1u;
// printf("Playback direction %d\n", playback_forwards);
} else if (c == '=' || c=='+') {
if (playback_speed < 3) playback_speed++;
// printf("Playback speed %d\n", playback_speed);
} else if (c== '-' || c=='_') {
if (playback_speed > -16) playback_speed--;
// printf("Playback speed %d\n", playback_speed);
} else if (c == '[') {
volume = MAX(volume - 8, 0);
// printf("Playback speed %d\n", playback_speed);
} else if (c== ']') {
volume = MIN(volume + 8, 0x100);
// printf("Playback speed %d\n", playback_speed);
} else if (c == ' ') {
ds.paused = !ds.paused;
// printf("Paused %d\n", ds.paused);
} else if (c == 'p' || c == '[' || c == '{') {
if (current_movie) {
current_movie--;
} else {
current_movie = movie_count-1;
}
display_base_frame = scanvideo_frame_number(scanvideo_get_next_scanline_id());
} else if (c == 'n' || c==']' || c == '}') {
current_movie++;
if (current_movie == movie_count) current_movie = 0;
display_base_frame = scanvideo_frame_number(scanvideo_get_next_scanline_id());
} else if (c=='i' || c=='\r') {
show_menu = !show_menu;
display_base_frame = scanvideo_frame_number(scanvideo_get_next_scanline_id());
} else if (c=='.') {
struct frame_header *head = (struct frame_header *)frame_header_sector;
// for now we'll force paused and regular speed
if (!ds.paused) ds.paused = true;
playback_speed = 0;
if (ds.paused && 0xffffffff != head->forward_frame_sector[0]) {
next_frame_sector_override = movies[registered_current_movie].start_sector + head->forward_frame_sector[0];
set_unpause();
}
} else if (c==',') {
struct frame_header *head = (struct frame_header *)frame_header_sector;
// for now we'll force paused and regular speed
if (!ds.paused) ds.paused = true;
playback_speed = 0;
if (ds.paused && 0xffffffff != head->backward_frame_sectors[0]) {
next_frame_sector_override = movies[registered_current_movie].start_sector + head->backward_frame_sectors[0];
set_unpause();
}
}
if (old_movie != current_movie) {
next_frame_sector_override = movies[current_movie].current_sector;
if (next_frame_sector_override < movies[current_movie].start_sector) {
next_frame_sector_override = movies[current_movie].start_sector;
}
set_unpause();
// ds.awaiting_first_frame = true;
// printf("setting override to %d\n", next_frame_sector_override);
}
}
if (ds.hold_frame)
{
if ((!ds.paused && --remaining_hold_frames <= 0) || ds.unpause)
{
if (ds.unpause) ds.unpause--;
ds.hold_frame = false;
#if PICO_NO_HARDWARE
remaining_hold_frames = 10;
#else
remaining_hold_frames = hold_frame_count;
#endif
} else {
popcorn_debug("======> poop %d\n", remaining_hold_frames);
}
}
else if (/*!ds.paused && */!ds.awaiting_first_frame)
{
uint new_frame = row_wrap_add(ds.rows.display_start_row, MOVIE_ROWS);
if (!row_index_in_range(new_frame, ds.rows.valid_from_row, ds.rows.valid_to_row)) {
// panic("b %d %d %d -> %d\n", ds.rows.valid_from_row, ds.rows.display_start_row, ds.rows.valid_to_row, new_frame);
// panic("frame not ready\n");
printf("%d frame not ready %d %d->%d %d valid %04x:%04x\n", (uint)ds.video_read.sector_base, ds.rows.valid_from_row, ds.rows.display_start_row, new_frame, ds.rows.valid_to_row, row_buffer_offsets[ds.rows.valid_from_row], ds.video_read.write_buffer_offset);
// __breakpoint();
puts("frame not ready\n");
ds.state = INIT;
if (ds.rows.valid_from_row == new_frame + 1) {
// panic("wadja");
}
} else
{
popcorn_debug("frame switch %d (%d->%d) %d %d+%d.%d\n", ds.rows.valid_from_row, ds.rows.display_start_row, new_frame, ds.rows.valid_to_row, ds.video_read.frame_base_row, ds.video_read.frame_row_count, ds.video_read.remaining_row_words);
ds.rows.display_start_row = new_frame;
this_hold_frame = ds.hold_frame = true;
#ifdef SILENCE_AUDIO_HACK
// todo temp hack until we hookup audio - just pretend we exhausted audio once per frame
ds.audio.playback_buffer_index = ds.audio.load_thread_buffer_index;
ds.audio.load_thread_buffer_index = ds.audio.load_thread_buffer_index++;
if (ds.audio.load_thread_buffer_index == NUM_AUDIO_BUFFERS) ds.audio.load_thread_buffer_index = 0;
ds.audio.buffer_state[ds.audio.load_thread_buffer_index] = BS_EMPTY;
#endif
}
}
}
if (!ds.awaiting_first_frame)
{
if (true)//!ds.paused)
{
int first_must_keep_row;
uint32_t other_core_scanline_id = last_scanline_id[0];
uint other_core_frame_number = scanvideo_frame_number(other_core_scanline_id);
if (this_hold_frame) {
popcorn_debug("%d chase reason hold_frame lsi %08x\n", ds.state, (uint)last_scanline_id[1]);
if (frame_num == other_core_frame_number)
{
first_must_keep_row = 0;
} else {
first_must_keep_row = -1;
}
} else
{
if (frame_num == other_core_frame_number)
{
first_must_keep_row = MIN(scanvideo_scanline_number(other_core_scanline_id),
scanvideo_scanline_number(last_scanline_id[1]));
}
else if (other_core_frame_number == (uint16_t) (frame_num + 1))
{
first_must_keep_row = scanvideo_scanline_number(last_scanline_id[1]);
}
else
{
first_must_keep_row = scanvideo_scanline_number(last_scanline_id[1]);
// if we are starving out core 0, then it may be stuck behind us... if we've made it SCANLINE_BUFFER/2 rows in
// then the old frame must be done now
if (first_must_keep_row <= PICO_SCANVIDEO_SCANLINE_BUFFER_COUNT / 2) {
first_must_keep_row = -1;
}
}
popcorn_debug("%d chase reason lsi %08x oc %08x fn %d ocfn %d behind %d, %d %d %d\n", ds.state, (uint)last_scanline_id[1],
(uint)other_core_scanline_id, frame_num, other_core_frame_number, first_must_keep_row,
ds.rows.valid_from_row, ds.rows.display_start_row, ds.rows.valid_to_row);
}
if (first_must_keep_row >= 0)
{
uint last_displayed_row = row_wrap_add(ds.rows.display_start_row, first_must_keep_row);
if (row_index_in_range(last_displayed_row, ds.rows.valid_from_row, ds.rows.valid_to_row))
{
popcorn_debug("%d zoom %d %d->%d %d %d\n", ds.state, this_hold_frame, ds.rows.valid_from_row, last_displayed_row, ds.rows.display_start_row, ds.rows.valid_to_row);
#ifdef DEBUG_UNKNOWN_ISSUE1
uint frame_row_count_limit = row_wrap_sub(ds.rows.valid_from_row, row_wrap_add(ds.video_read.frame_base_row, 1));
if (ds.video_read.frame_row_count > frame_row_count_limit) {
printf("Ooble arble %d %d\n", ds.video_read.frame_row_count, frame_row_count_limit);
}
#endif
ds.rows.valid_from_row = last_displayed_row;
#ifdef DEBUG_UNKNOWN_ISSUE1
frame_row_count_limit = row_wrap_sub(ds.rows.valid_from_row, row_wrap_add(ds.video_read.frame_base_row, 1));
if (ds.video_read.frame_row_count > frame_row_count_limit) {
printf("Ooble %d %d\n", ds.video_read.frame_row_count, frame_row_count_limit);
}
#endif
}
else
{
// panic("underran rows");
popcorn_debug("XXXXXX UNDERRAN ROWS\n");
}
} else {
popcorn_debug("ZZZ\n");
}
}
}
sd_state_update();
} else if (show_menu) {
if (text_roller<MENU_GLYPH_HEIGHT) {
static struct text_element *last_element;
struct text_element *element;
uint count = (frame_num - display_base_frame) >> 7;
if (count & 1u) {
count >>= 1;
count = hw_divider_u32_remainder_inlined(count, count_of(instructions));
element = instructions + count;
} else {
element = &movies[registered_current_movie].text;
}
if (text_roller || element != last_element)
{
uint16_t *out = (uint16_t *) (overlay + (text_roller + 3 + lcd18.line_height) * OVERLAY_WIDTH);
if (!element->width)
{
measure_text(element, OVERLAY_WIDTH * 2);
// printf("Measure '%s' %d\n", element->text, element->width);
}
uint off = 3 + OVERLAY_WIDTH - element->width / 2;
__builtin_memset(out, 0, off * 2);
uint len = render_font_line(element, out + off, menu_glypth_bitmap + text_roller, element->color);
if (off + len < OVERLAY_WIDTH * 2)
{
__builtin_memset(out + off + len, 0, (OVERLAY_WIDTH * 2 - off - len) * 2);
}
// out[0] = 0x1e0;
// out[element->width] = 0xf;
text_roller++;
}
if (text_roller == MENU_GLYPH_HEIGHT) {
last_element = element;
}
}
}
// we must latch this now under lock so we have the right value on core 0 (core 1 may change it afterwards)
this_display_start_row = ds.rows.display_start_row;
mutex_exit(&frame_logic_mutex);
uint16_t scanline_num = scanvideo_scanline_number(sb[0]->scanline_id);
if (!core_num && core_0_beat_core_1_to_new_frame) {
// we just do a simple hack to move to the new frame... core 0 does not maintain locks sufficient to interact with core 1 state.
// this is reasonable as we are about to start drawing the frame anyway.
this_display_start_row = row_wrap_add(this_display_start_row, MOVIE_ROWS);
}
DEBUG_PINS_SET(frame_generation, (core_num) ? 2 : 4);
#ifdef SHOW_PERF_COUNTERS
int l = scanline_number(scanline_buffer->scanline_id);
if (l < 16 / vga_mode.yscale) {
int w = l >= (8 / vga_mode.yscale);
__builtin_memcpy(scanline_buffer->data, bar_chart[core_num][w], bar_chart_size[core_num][w] * 4);
scanline_buffer->data_used = bar_chart_size[core_num][w];
__builtin_memcpy(sb[1]->data, bar_chart[core_num][w], bar_chart_size[core_num][w] * 4);
sb[1]->data_used = bar_chart_size[core_num][w];
} else {
#endif
uint16_t *buf16_0 = (uint16_t *) sb[0]->data;
uint16_t *buf16_1 = (uint16_t *) sb[1]->data;
uint row_number = scanline_num;
uint row_index = row_wrap_add(this_display_start_row, row_number);
bool row_valid = row_index_in_range(row_index, ds.rows.valid_from_row, ds.rows.valid_to_row);
uint pos;
// todo turning this off breaks muse, so that implies a timing/state DEBUG_UNKNOWN_ISSUE1
#ifdef DEBUG_UNKNOWN_ISSUE1
if (row_valid && !show_menu)
#else
if (row_valid)
#endif
{
if (row_index >= ROW_OFFSET_CIRCLE_SIZE) {
printf("WTF rn %d+%d %d %d %d\n", this_display_start_row, row_number, ds.rows.valid_from_row, row_index, ds.rows.valid_to_row);
}
assert(row_index < ROW_OFFSET_CIRCLE_SIZE);
assert(row_buffer_offsets[row_index] < IMAGE_DATA_WORDS);
const uint32_t *compressed_scanline = image_data + row_buffer_offsets[row_index];
const int w = 320;
const __unused uint32_t *end;
if (core_num)
{
end = platypus_decompress_row_b(sb[0]->data + 1, sb[1]->data + 1, compressed_scanline, w);
__unused struct frame_header *header = (struct frame_header *)frame_header_sector;
check_debug(end, header, row_number);
if (show_menu && row_number >= OVERLAY_START && row_number < OVERLAY_END) {
darken_a(sb[0]->data + OVERLAY_X, sb[1]->data - sb[0]->data, overlay + (row_number - OVERLAY_START) * OVERLAY_WIDTH, OVERLAY_WIDTH);
}
} else
{
end = platypus_decompress_row_a(sb[0]->data + 1, sb[1]->data + 1, compressed_scanline, w);
__unused struct frame_header *header = (struct frame_header *)frame_header_sector;
check_debug(end, header, row_number);
if (show_menu && row_number >= OVERLAY_START && row_number < OVERLAY_END) {
darken_b(sb[0]->data + OVERLAY_X, sb[1]->data - sb[0]->data, overlay + (row_number - OVERLAY_START) * OVERLAY_WIDTH, OVERLAY_WIDTH);
}
}
#ifdef ENABLE_STRICT_ASSERTIONS
const uint16_t *compressed_scanline_owning_row = ram_buffer_owning_row + row_buffer_offsets[row_index];
uint compressed_len = end - compressed_scanline;
assert(compressed_len <= (w * 7) / 8);
for(int x = 0; x < compressed_len; x++) {
assert(compressed_scanline_owning_row[x] == row_index);
}
#endif
buf16_0[1] = buf16_0[2];
buf16_1[1] = buf16_1[2];
buf16_0[2] = buf16_1[2] = w - 3;
buf16_0[0] = buf16_1[0] = COMPOSABLE_RAW_RUN;
pos = w + 2;
}
else
{
pos = 0; // blank display
buf16_0[pos] = buf16_1[pos] = COMPOSABLE_RAW_1P;
pos++;
buf16_0[pos] = buf16_1[pos] = 0x7c1f;
pos++;
}
#ifndef SHOW_PERF_COUNTERS
// if (frame_number(sb[0]->scanline_id) < 2)
#endif
{
buf16_0[pos] = buf16_1[pos] = COMPOSABLE_RAW_1P;
pos++;
buf16_0[pos] = buf16_1[pos] = 0;
pos++;
buf16_0[pos] = buf16_1[pos] = COMPOSABLE_EOL_SKIP_ALIGN;
pos++;
buf16_0[pos] = buf16_1[pos] = 0xffff;
pos++;
assert(!(pos & 1u));
sb[0]->data_used = sb[1]->data_used = pos / 2;
}
sb[0]->status = sb[1]->status = SCANLINE_OK;
#ifdef SHOW_PERF_COUNTERS
}
#endif
DEBUG_PINS_CLR(frame_generation, (core_num) ? 2 : 4);
last_scanline_id[core_num] = sb[0]->scanline_id;
scanvideo_end_scanline_generation(sb[0]); // sb[1] is linked
}
}
struct semaphore video_setup_complete;
void setup_video() {
scanvideo_setup(&vga_mode);
scanvideo_timing_enable(true);
sem_release(&video_setup_complete);
}
void core1_func() {
#ifdef RENDER_ON_CORE1
init_render_state(1);
#endif
#ifdef IRQS_ON_CORE1
setup_video();
#else
sem_acquire_blocking(&video_setup_complete);
#endif
#ifdef RENDER_ON_CORE1
render_loop();
#endif
}
#define TEST_WAIT_FOR_SCANLINE
#ifdef TEST_WAIT_FOR_SCANLINE
volatile uint32_t scanline_color = 0;
#endif
int video_main(void) {
font12 = build_font(&lcd12, 4 + hack());
font18 = build_font(&lcd18, 5);
#ifdef ENABLE_STRICT_ASSERTIONS
memset(ram_buffer_owning_row, 0xee, sizeof(ram_buffer_owning_row));
#endif
#if 0
uint8_t *new_frame = (uint8_t *)malloc(*frame_size);
// extern char __StackLimit;
// uint8_t *new_frame = &__StackLimit - *frame_size;
// new_frame = 0x20010000;
printf("%p %p\n", new_frame, new_frame + *frame_size);
__builtin_memcpy(new_frame, frame, *frame_size);
frame = new_frame;
#endif
mutex_init(&frame_logic_mutex);
sem_init(&video_setup_complete, 0, 1);
#if PICO_ON_DEVICE
// bus_ctrl_hw->priority = BUSCTRL_BUS_PRIORITY_DMA_R_BITS;
#endif
ds.state = INIT;
ds.awaiting_first_frame = true;
init_render_state(0);
struct audio_format platypus_audio_format = {
.format = AUDIO_BUFFER_FORMAT_PCM_S16,
.sample_freq = 44100,
.channel_count = 2,
};
struct audio_buffer_format producer_format = {
.format = &platypus_audio_format,
.sample_stride = 4
};
// todo we will need to fill in some buffers!
audio_buffer_pool = audio_new_producer_pool(&producer_format, 0, 0);
struct audio_i2s_config config = {
.data_pin = PICO_AUDIO_I2S_DATA_PIN,
.clock_pin_base = PICO_AUDIO_I2S_CLOCK_PIN_BASE,
.dma_channel = 1,
#if PICO_AUDIO_I2S_PIO == 0
.pio_sm = 2,
#else
.pio_sm = 3,
#endif
};
for(int i=0; i<NUM_AUDIO_BUFFERS; i++) {
audio_buffers[i] = audio_new_wrapping_buffer(&producer_format, pico_buffer_wrap((uint8_t *)audio_buffer_start[i], AUDIO_BUFFER_K * 1024));
}
const struct audio_format *output_format;
output_format = audio_i2s_setup(&platypus_audio_format, &config);
if (!output_format) {
panic("MuAudio: Unable to open audio device.");
}
bool ok = audio_i2s_connect_thru(audio_buffer_pool, &popcorn_passthru_connection);
if (!ok) {
panic("failed to connect audio");
}
// for now just make it play silence
// todo for now this brings everything falling down! - sometimes we get horribly out of sync stuff
// audio_i2s_enable(true);
// for(int i=0;i<5000;i++) {
// printf("%d\n", i);
// }
#ifdef JUST_READ
// see what happens without video
while (true) {
sd_state_update();
}
#endif
#if defined(RENDER_ON_CORE1) || defined(IRQS_ON_CORE1)
multicore_launch_core1(core1_func);
#endif
#ifndef IRQS_ON_CORE1
setup_video();
#if PICO_ON_DEVICE
// todo revist; this is actually the default priority now!
irq_set_priority(DMA_IRQ_1, 0x80); // lower priority by 2
#endif
#endif
#ifdef RENDER_ON_CORE0
render_loop();
#else
sem_acquire_blocking(&video_setup_complete);
while (true) {
#ifndef TEST_WAIT_FOR_SCANLINE
// Just use vblank to print out a value every second
static int i=0, s=0;
video_wait_for_vblank();
if (++i == 60) {
printf("%d\n", s++);
i = 0;
}
#else
static uint32_t sl = 0;
sl = video_wait_for_scanline_complete(sl);
scanline_color = (scanline_color + 0x10u) & 0xffu;
#endif
}
#endif
__builtin_unreachable();
}
// must not be called concurrently
void init_render_state(int core) {
#if PICO_ON_DEVICE
platypus_decompress_configure_interp(core);
#endif
if (core) {
audio_i2s_set_enabled(true);
}
}
int main(void) {
#if PICO_ON_DEVICE
set_sys_clock_48mhz();
#endif
// gpio_put(27, 0);
// gpio_dir_in_mask(1<<28);
#ifdef USE_RGB_LOW_FOR_DEBUG_PINS
gpio_dir_out_mask(0x421);
#endif
setup_default_uart();
#ifdef VGABOARD_BUTTON_A_PIN
vga_board_init_buttons();
#endif
return video_main();
}