#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "engine.h" #include "compiled_structures.h" #include "objids.h" #define PLAYFIELD_WIDTH 1280 #define PLAYFIELD_HEIGHT 800 static int touchscreen_fd; static struct navmesh null_navmesh = {0}; struct scene scenes[MAX_STACKED_SCENES]; int scene_depth = 0; bool need_rerender = false; struct savefile savefile; #define MAX_MT_SLOTS 16 static struct mtslot { int x, y, touch, lasttouch; int lastx, lasty; } mtslots[MAX_MT_SLOTS] = {0}; static struct mtslot *curslot = &mtslots[0]; // TODO: query the current value of ABS_MT_SLOT static void poll_touchscreen() { struct input_event buf[64]; int nread; retry_read: nread = read(touchscreen_fd, buf, sizeof buf); if(nread < 0) { if(errno == EINTR) goto retry_read; if(errno == EAGAIN) return; error(1, errno, "read touchscreen"); } if(nread % sizeof(struct input_event)) error(1, 0, "odd size read from touchscreen"); for(int i = 0; i < nread; i += sizeof(struct input_event)) { struct input_event *evt = (struct input_event*)((char*)buf + i); //printf("type=%d code=%d value=%d\n", evt->type, evt->code, evt->value); if(evt->type == EV_SYN && evt->code == SYN_DROPPED) error(0, 0, "got SYN_DROPPED"); // TODO: resynchronize if(evt->type == EV_SYN && evt->code == SYN_REPORT) { for(int j = 0; j < MAX_MT_SLOTS; j++) { struct mtslot *s = &mtslots[j]; if(s->touch) { //printf("slot %d x=%d y=%d\n", j, s->x, s->y); /*uint32_t col; switch(j & 3) { case 0: col = 0xFFFFFFFF; break; case 1: col = 0xFF0000FF; break; case 2: col = 0x00FF00FF; break; case 3: col = 0x0080FFFF; break; } int segs = s->lastx<0 ? 1 : (int)(0.9 + 2*sqrt((s->lastx-s->x)*(s->lastx-s->x) + (s->lasty-s->y)*(s->lasty-s->y))); if(segs<1) segs=1; for(int f = 0; f <= segs; f++) { int _x, _y; if(s->lastx != -1) { _x = (s->lastx + (s->x-s->lastx)*f/segs); _y = (s->lasty + (s->y-s->lasty)*f/segs); } else { _x = s->x; _y = s->y; } for(int dy = -5; dy <= 5; dy++) { for(int dx = -5; dx <= 5; dx++) { if(dx*dx + dy*dy <= 5*5) { int x = dx+_x, y = dy+_y; if(x >= 0 && y >= 0 && x < 800 && y < 1280) { *(uint32_t*)(fb + (y*800 + x)) = col; } } } } }*/ if(!s->lasttouch) { top_scene.handle_tap_fn(&top_scene, s->y, 799-s->x); } } s->lasttouch = s->touch; s->lastx = s->x; s->lasty = s->y; } } if(evt->type == EV_ABS && evt->code == ABS_MT_SLOT) { if(evt->value >= 0 && evt->value < MAX_MT_SLOTS) curslot = &mtslots[evt->value]; else curslot = NULL; } if(evt->type == EV_ABS && evt->code == ABS_MT_POSITION_X) { if(curslot) curslot->x = evt->value; } if(evt->type == EV_ABS && evt->code == ABS_MT_POSITION_Y) { if(curslot) curslot->y = evt->value; } if(evt->type == EV_ABS && evt->code == ABS_MT_TRACKING_ID) { if(curslot) { if(!curslot->touch) { curslot->lastx = curslot->lasty = -1; } curslot->touch = evt->value != -1; } } } } // Rendering is dumb, double-buffered with page flip, always redrawing the whole frame. // It could be improved. uint32_t current_animframe; uint32_t *curfb; // used during render cycle void blit(int x, int y, int width, int height, uint32_t *pixels) { uint32_t *inbase = pixels; //printf("blit %dx%d at %d,%d\n", spr->width, spr->height, x, y); y = PLAYFIELD_HEIGHT - height - y; if(y >= PLAYFIELD_HEIGHT || x >= PLAYFIELD_WIDTH || y <= -height || x <= -width) return; if(height > PLAYFIELD_HEIGHT) error(1, 0, "sprite bigger than playfield not supported"); // would need to clip top and bottom simultaneously - not implemented for now uint32_t *outbase = curfb + x*PLAYFIELD_HEIGHT + y; for(int dx = 0; dx < width; dx++, inbase += height, outbase += PLAYFIELD_HEIGHT, x++) { if(x < 0) continue; if(x >= PLAYFIELD_WIDTH) break; if(y < 0) { // TODO: loop inversion would make sense int offset = -y; // must have at least one pixel to copy memcpy(outbase+offset, inbase+offset, (height-offset)*4); } else { int ncopy = PLAYFIELD_HEIGHT-y; if(ncopy > height) ncopy = height; memcpy(outbase, inbase, ncopy*4); } } } void fillrect(int x1, int y1, int width, int height, uint32_t pixel) { int x2 = x1 + width; int y2 = y1 + height; if(x1 < 0) x1 = 0; if(y1 < 0) y1 = 0; if(x2 >= PLAYFIELD_WIDTH) x2 = PLAYFIELD_WIDTH; if(y2 >= PLAYFIELD_HEIGHT) y2 = PLAYFIELD_HEIGHT; y1 = PLAYFIELD_HEIGHT-1-y1; y2 = PLAYFIELD_HEIGHT-1-y2; for(int x = x1; x < x2; x++) { uint32_t *outbase = curfb + x*PLAYFIELD_HEIGHT + y2; for(int y = y2; y < y1; y++) { *outbase++ = pixel; } } } static void scene_clear_and_setup(int scene, int fromscene, scene_setup_fn setup_fn) { struct scene *me = &top_scene; // setup_fn might push additional scenes such as textboxes memset(me, 0, sizeof(*me)); me->id = scene; me->navmesh = &null_navmesh; me->handle_tap_fn = standard_handle_tap; me->render_fn = standard_scene_render; me->use_standard_inventory = true; setup_fn(me, scene, fromscene); if(me->use_standard_inventory) { create_standard_inventory(me); } need_rerender = true; } void push_scene(int scene, scene_setup_fn setup_fn) { scene_depth++; if(scene_depth > MAX_STACKED_SCENES) error(1, 0, "maximum scene depth exceeded"); scene_clear_and_setup(scene, -1, setup_fn); } void push_scene(int scene) { push_scene(scene, scene_setup); } void replace_scene(int scene) { scene_clear_and_setup(scene, top_scene.id, scene_setup); } void pop_scene() { if(scene_depth == 1) error(1, 0, "no scenes left"); scene_depth--; // any cleanup? need_rerender = true; } struct object *scene_add_object(struct scene *sc, int id, int x, int y, int width, int height, const char *pixels) { if(id == 0) error(1, 0, "scene_add_object: id 0 is invalid"); struct object *objects = sc->objects; for(int i = 0; i < MAX_OBJECTS_PER_SCENE; i++) { if(objects[i].id == 0) { objects[i].id = id; objects[i].x = x; objects[i].y = y; objects[i].width = width; objects[i].height = height; objects[i].pixels = pixels ? (const char*)get_decompressed_sprite((const unsigned char*)pixels)->pixels : nullptr; return &objects[i]; } } error(1, 0, "too many game objects"); } void scene_load_predef(struct scene *sc, const struct level_predef_data *predef) { for(const struct level_clickregion *cr = predef->clickregions; cr->num_edges; cr++) { struct object *obj = scene_add_object(sc, cr->id, cr->x, cr->y, cr->width, cr->height, nullptr); obj->clickregion = cr; } } struct object *find_object_by_id(int id) { struct object *objects = top_scene.objects; for(int i = 0; i < MAX_OBJECTS_PER_SCENE; i++) { if(objects[i].id == id) return &objects[i]; } return nullptr; } struct script *scene_add_script(int id, bool interrupt_existing) { struct script *scripts = top_scene.scripts; for(int i = 0; i < MAX_SCRIPTS_PER_SCENE; i++) { if(scripts[i].id == id && interrupt_existing) { // interrupt existing script? // player walk script is safe to trivially cancel, at least scripts[i].id = 0; if(deliver_script_wakeup(SCRIPT_WAKEUP_OTHER_SCRIPT, id, SCRIPT_WAKEUP_OTHER_SCRIPT_INTERRUPTED, 0, 0, 0, 0)) return NULL; // scene changed. this probably crashes the caller. should be tested or we need a better way to cancel all running operations on scene change. } if(scripts[i].id == 0) { memset(&scripts[i], 0, sizeof(scripts[i])); scripts[i].id = id; return &scripts[i]; } } error(1, 0, "too many game scripts"); __builtin_unreachable(); } static void closest_point_inside_tri(int *x, int *y, const struct navmesh_tri *tri) { double fx = *x, fy = *y; int edgeHitMask = 0; // Moving a point outside of one half-plane can move it into another half-plane, so check again. // If this happens, we must be in a corner region. // Repeated normal-vector moves don't necessarily bring us to the corner, but we can just hardcode the corner. for(int j = 0; j < 2; j++) { for(int i = 0; i < tri->num_verts; i++) { double edgeval = fx*tri->edges[i].a + fy*tri->edges[i].b + tri->edges[i].c; if(edgeval <= 0) { // edge.a,b is normalized so that moving 1 unit of a and 1 unit of b changes edgeval by 1. fx -= tri->edges[i].a*edgeval; fy -= tri->edges[i].b*edgeval; edgeHitMask |= 1 << i; } } } for(int i = 0; i < tri->num_verts; i++) { int next = (i+1) % tri->num_verts; int mask = (1 << i) | (1 << next); if((edgeHitMask & mask) == mask) { *x = tri->points[next].x; *y = tri->points[next].y; return; } } /* switch(edgeHitMask) { case 3: // hit edge 0-1 and 1-2 *x = tri->points[1].x; *y = tri->points[1].y; return; case 5: // hit edge 0-1 and 2-0 *x = tri->points[0].x; *y = tri->points[0].y; return; case 6: // hit edge 1-2 and 2-0 *x = tri->points[2].x; *y = tri->points[2].y; return; }*/ // may be slightly outside of triangle due to rounding errors, but good enough for us *x = (int)(fx + 0.5); *y = (int)(fy + 0.5); } static double point_to_point_dist(int x, int y, const struct navmesh_point *pt) { x -= pt->x; y -= pt->y; return sqrt(x*x + y*y); } // Returns distance from triangle, 0 if inside. static double is_point_in_tri(int x, int y, const struct navmesh_tri *tri) { /*double edgedist[3]; // positive if outside for(int edge = 0; edge < 3; edge++) edgedist[edge] = -(x*tri->edges[edge].a + y*tri->edges[edge].b + tri->edges[edge].c); if(edgedist[0] < 0 && edgedist[1] < 0 && edgedist[2] < 0) return 0; // inside if(edgedist[0] >= 0 && edgedist[1] >= 0) return point_to_point_dist(x, y, &tri->points[0]); // are these correct? shouldn't it be points 1,2,0? if(edgedist[1] >= 0 && edgedist[2] >= 0) return point_to_point_dist(x, y, &tri->points[1]); if(edgedist[2] >= 0 && edgedist[0] >= 0) return point_to_point_dist(x, y, &tri->points[2]); if(edgedist[0] >= 0) return edgedist[0]; if(edgedist[1] >= 0) return edgedist[1]; if(edgedist[2] >= 0) return edgedist[2];*/ double edgedist[tri->num_verts]; bool all_inside = true; for(int edge = 0; edge < tri->num_verts; edge++) { edgedist[edge] = -(x*tri->edges[edge].a + y*tri->edges[edge].b + tri->edges[edge].c); if(edgedist[edge] > 0) all_inside = false; } if(all_inside) return 0; for(int edge = 0; edge < tri->num_verts; edge++) { int next = (edge+1)%tri->num_verts; if(edgedist[edge] >= 0 && edgedist[next] >= 0) return point_to_point_dist(x, y, &tri->points[next]); } for(int edge = 0; edge < tri->num_verts; edge++) if(edgedist[edge] >= 0) return edgedist[edge]; return 0; // branch should be unreachable; maybe within epsilon of triangle border? } static int find_navmesh_tri(int x, int y, int tolerance) { int best_tri = -1; double best_dist = tolerance; struct navmesh *cur_navmesh = top_scene.navmesh; for(int i = 0; i < cur_navmesh->num_tris; i++) { double dist = is_point_in_tri(x, y, &cur_navmesh->tris[i]); if(dist == 0) return i; if(dist < best_dist) { best_dist = dist; best_tri = i; } } return best_tri; } static void update_player_walk_script_on_frame(struct script *scr, int wakeupMode, int, int, int, int); static uint32_t player_walk_generation = 0; void start_player_walk_to_point(int targetX, int targetY) { player_walk_generation++; struct object *player = find_object_by_id(OBJID_PLAYER); if(!player) error(1, 0, "start_player_walk_to_point: no player object"); int destTri = find_navmesh_tri(targetX, targetY, 999999); if(destTri == -1) error(1, 0, "start_player_walk_to_point: no navmesh"); struct script_player_walk *player_walk_script = (struct script_player_walk *)scene_add_script(OBJID_PLAYER_WALK_SCRIPT, true); player_walk_script->targetX = targetX; player_walk_script->targetY = targetY; player_walk_script->targetNavmeshTri = destTri; player_walk_script->wakeupMode = SCRIPT_WAKEUP_VIDEO_FRAME; player_walk_script->wakeupFn = update_player_walk_script_on_frame; int sourceTri = find_navmesh_tri(player->x+player->width*2, player->y+player->height, 1000000); player_walk_script->currentNavmeshTri = sourceTri; printf("set course from triangle %d to %d\n", sourceTri, destTri); } void standard_handle_tap(struct scene *scene, int x, int y) { uint32_t walkgen = player_walk_generation; int sceneid = scene->id; struct navmesh *cur_navmesh = top_scene.navmesh; struct object *objects = top_scene.objects; for(int i = 0; i < MAX_OBJECTS_PER_SCENE; i++) { if(!objects[i].id) continue; if(objects[i].clickregion) { const struct level_clickregion *cr = objects[i].clickregion; for(int i = 0; i < cr->num_edges; i++) { if(cr->edges[i].a*x + cr->edges[i].b*y + cr->edges[i].c < 0) goto click_not_on_object; } } else { if((unsigned int)(x - objects[i].x) >= objects[i].width || (unsigned int)(y - objects[i].y) >= objects[i].height) goto click_not_on_object; } // clicked on the object onclick(sceneid, &objects[i]); if(top_scene.id != sceneid) return; // early exit if scene changed, and don't walk the player either click_not_on_object:; } if(walkgen == player_walk_generation) { // no other handler moved the player struct object *player = find_object_by_id(OBJID_PLAYER); if(player) { int destTri = find_navmesh_tri(x, y, 50); if(destTri != -1) { int targetX = x, targetY = y; closest_point_inside_tri(&targetX, &targetY, &cur_navmesh->tris[destTri]); start_player_walk_to_point(targetX, targetY); } } } } static int move_towards(int cur, int target, int maxrate) { if(target > cur+maxrate) return cur+maxrate; if(target < cur-maxrate) return cur-maxrate; return target; } static void update_player_walk_script_on_frame(struct script *scr, int wakeupMode, int, int, int, int) { int script_id = scr->id; struct script_player_walk *player_walk_script = (struct script_player_walk *)scr; if(!player_walk_script) return; struct object *player = find_object_by_id(OBJID_PLAYER); if(!player) { player_walk_script->id = 0; deliver_script_wakeup(SCRIPT_WAKEUP_OTHER_SCRIPT, script_id, SCRIPT_WAKEUP_OTHER_SCRIPT_INTERRUPTED, 0, 0, 0, 0); return; } struct navmesh *cur_navmesh = top_scene.navmesh; if(player_walk_script->currentNavmeshTri == -1) { // something weird happened. just teleport player and end movement. player->x = player_walk_script->targetX - player->width/2; player->y = player_walk_script->targetY - player->height; player_walk_script->id = 0; deliver_script_wakeup(SCRIPT_WAKEUP_OTHER_SCRIPT, script_id, SCRIPT_WAKEUP_OTHER_SCRIPT, 0, 0, 0, 0); need_rerender = true; } else { int x = player->x+player->width/2; int y = player->y+player->height; int nmtri = player_walk_script->currentNavmeshTri; int nextX; int nextY; int nextTri = -1; if(nmtri == player_walk_script->targetNavmeshTri) { nextX = player_walk_script->targetX; nextY = player_walk_script->targetY; } else { nextTri = cur_navmesh->pathfindgrid[player_walk_script->targetNavmeshTri * cur_navmesh->num_tris + nmtri]; nextX = -1; for(int edge = 0; edge < cur_navmesh->tris[nmtri].num_verts; edge++) { if(cur_navmesh->tris[nmtri].edges[edge].other_tri == nextTri) { nextX = cur_navmesh->tris[nmtri].edges[edge].center.x; nextY = cur_navmesh->tris[nmtri].edges[edge].center.y; goto center_found; } } // shouldn't happen player_walk_script->id = 0; deliver_script_wakeup(SCRIPT_WAKEUP_OTHER_SCRIPT, script_id, SCRIPT_WAKEUP_OTHER_SCRIPT_INTERRUPTED, 0, 0, 0, 0); return; center_found:; } if(nextX == x && nextY == y) { printf("in triangle %d, next triangle %d\n", nmtri, nextTri); if(x == player_walk_script->targetX && y == player_walk_script->targetY) { player_walk_script->id = 0; deliver_script_wakeup(SCRIPT_WAKEUP_OTHER_SCRIPT, script_id, SCRIPT_WAKEUP_OTHER_SCRIPT, 0, 0, 0, 0); } else { player_walk_script->currentNavmeshTri = nextTri; // even if -1 } } else { x = move_towards(x, nextX, 5); y = move_towards(y, nextY, 2); player->x = x - player->width/2; player->y = y - player->height; } need_rerender = true; } } bool deliver_script_wakeup(int wakeupMode, int wakeupArg1, int wakeupType, int arg1, int arg2, int arg3, int arg4) { int scene = top_scene.id; struct script *scripts = top_scene.scripts; for(int i = 0; i < MAX_SCRIPTS_PER_SCENE; i++) { if(scripts[i].id && scripts[i].wakeupMode == wakeupMode && scripts[i].wakeupArg1 == wakeupArg1) { scripts[i].wakeupFn(&scripts[i], wakeupType, arg1, arg2, arg3, arg4); if(scene_depth == 0 || top_scene.id != scene) return true; // scene changed - abort early } } return false; } void standard_scene_render(scene *s) { if(s->dim_background) { uint32_t *ptr = curfb; for(int i = 0; i < 1280*800; i++, ptr++) { *ptr = (*ptr >> 2) & 0x3f3f3f; } } struct object *objects = s->objects; for(int i = 0; i < MAX_OBJECTS_PER_SCENE; i++) { if(objects[i].id != 0 && objects[i].pixels) { blit(objects[i].x, objects[i].y, objects[i].width, objects[i].height, (uint32_t*)objects[i].pixels); } } } int main(int argc, char **argv) { if(getuid() != 0) { execlp("sudo", "sudo", argv[0], NULL); error(1, errno, "execlp"); return 1; } int fd = open("/dev/dri/card0", O_RDWR | O_NONBLOCK); if(fd < 0) error(1, errno, "open display"); touchscreen_fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK); if(touchscreen_fd < 0) error(1, errno, "open touchscreen"); if(ioctl(fd, DRM_IOCTL_SET_MASTER, NULL) < 0) error(1, errno, "set master"); // attach CRTC 51, connector 56, to a new framebuffer with a new dumbbuffer // create two buffers uint32_t *fbs[2]; uint32_t fb_ids[2]; for(int i = 0; i < 2; i++) { struct drm_mode_create_dumb createdumb = { .height = 1280, .width = 800, .bpp = 32, .flags = 0, }; if(ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &createdumb) < 0) error(1, errno, "create dumb buffer"); if(createdumb.pitch != 800*4 || createdumb.size != 1280*800*4) error(1, 0, "unexpected size for dumb buffer (pitch=%d size=%d)", (int)createdumb.pitch, (int)createdumb.size); struct drm_mode_fb_cmd2 addfb = { .width = 800, .height = 1280, .pixel_format = DRM_FORMAT_XRGB8888, .flags = 0, .handles = {createdumb.handle, 0, 0, 0}, .pitches = {createdumb.pitch, 0, 0, 0}, .offsets = {0, 0, 0, 0}, }; if(ioctl(fd, DRM_IOCTL_MODE_ADDFB2, &addfb) < 0) error(1, errno, "add framebuffer"); struct drm_mode_map_dumb mapdumb = { .handle = createdumb.handle }; if(ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mapdumb) < 0) error(1, errno, "map dumb buffer"); fbs[i] = (uint32_t*)mmap(NULL, createdumb.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, mapdumb.offset); if(!fbs[i]) error(1, errno, "mmap dumb buffer"); fb_ids[i] = addfb.fb_id; memset(fbs[i], 0, createdumb.size); } uint32_t connectors[1] = {56}; struct drm_mode_crtc crtc = {.crtc_id = 51}; if(ioctl(fd, DRM_IOCTL_MODE_GETCRTC, &crtc) < 0) error(1, errno, "get crtc"); //struct drm_mode_crtc oldcrtc = crtc; crtc.set_connectors_ptr = (uint64_t)connectors; crtc.count_connectors=1; crtc.fb_id = fb_ids[0]; if(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &crtc) < 0) error(1, errno, "set crtc"); int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if(timerfd < 0) error(1, errno, "timerfd_create"); { #define ANIMTIMER_INTERVAL 75000000 struct itimerspec tspec = {.it_interval = {.tv_nsec = ANIMTIMER_INTERVAL}, .it_value = {.tv_nsec = ANIMTIMER_INTERVAL}}; if(timerfd_settime(timerfd, 0, &tspec, NULL) < 0) error(1, errno, "timerfd_settime"); } push_scene(SCENE_LOBBY); int cur_buffer = 0; need_rerender = true; bool awaiting_vblank = false; for(;;) { struct pollfd polls[] = { {.fd = touchscreen_fd, .events = POLLIN}, {.fd = timerfd, .events = POLLIN}, {.fd = fd, .events = POLLIN}, }; const int NPOLLS = sizeof(polls)/sizeof(polls[0]); static const char *pollfd_labels[NPOLLS] = {"touchscreen", "animation timer", "display"}; int npoll = poll(polls, NPOLLS, 0); if(npoll < 0) error(1, errno, "poll"); // POLLERR, POLLHUP, POLLNVAL can always be returned for(int i = 0; i < NPOLLS; i++) { if(polls[i].revents & (POLLNVAL | POLLERR | POLLHUP)) { if(polls[i].revents & POLLNVAL) error(1, 0, "poll: got POLLNVAL (%s)", pollfd_labels[i]); if(polls[i].revents & POLLERR) error(1, 0, "poll: got POLLERR (%s)", pollfd_labels[i]); if(polls[i].revents & POLLHUP) error(1, 0, "poll: got POLLHUP (%s)", pollfd_labels[i]); } } if(polls[0].revents & POLLIN) { poll_touchscreen(); need_rerender = true; // TODO not always } if(polls[1].revents & POLLIN) { uint64_t dummy; // don't care about expiration count if(read(timerfd, &dummy, sizeof dummy) < 0) error(1, errno, "read (timerfd)"); current_animframe++; //printf("animation frame %u\n", (unsigned int)current_animframe); if(top_scene.animtimer_fn) { top_scene.animtimer_fn(&top_scene); } } if(polls[2].revents & POLLIN) { char buf[256]; int nread = read(fd, buf, sizeof buf); if(nread < 0) error(1, errno, "read (display)"); char *pos = buf; while(pos < buf + nread) { struct drm_event *evthdr = (struct drm_event*)pos; pos += evthdr->length; switch(evthdr->type) { case DRM_EVENT_FLIP_COMPLETE: awaiting_vblank = false; break; default: fprintf(stderr, "unknown DRM event type 0x%08" PRIx32, (unsigned int)evthdr->type); break; } } } if(!awaiting_vblank) { // return value ignored // What is this really doing? It's giving scripts the opportunity to trigger a video frame. // But if no script actually does, this can run over and over many times in the same video frame. // It's not saying a video frame happened, it's saying one can happen. deliver_script_wakeup(SCRIPT_WAKEUP_VIDEO_FRAME, 0, SCRIPT_WAKEUP_VIDEO_FRAME, 0, 0, 0, 0); } if(need_rerender && !awaiting_vblank) { cur_buffer = 1-cur_buffer; // for testing, just keep writing into the same buffer curfb = fbs[cur_buffer]; for(int s = 0; s < scene_depth; s++) { scenes[s].render_fn(&scenes[s]); } struct drm_mode_crtc_page_flip flipcmd = { .crtc_id = crtc.crtc_id, .fb_id = fb_ids[cur_buffer], .flags = DRM_MODE_PAGE_FLIP_EVENT, // async flip doesn't work on pinetab }; if(ioctl(fd, DRM_IOCTL_MODE_PAGE_FLIP, &flipcmd) < 0) error(1, errno, "DRM page flip"); awaiting_vblank = true; need_rerender = false; } } //if(ioctl(fd, DRM_IOCTL_MODE_SETCRTC, &oldcrtc) < 0) error(1, errno, "restore crtc"); // doesn't work: EINVAL (maybe because framebuffer id is local to our connection) }