rt_replay/rt_replay.h

455 lines
13 KiB
C

#ifndef RT_REPLAY_H
#define RT_REPLAY_H
/* rt_replay.h
* Input replay system for game development.
*
* This library is C99 compliant and builds without warnings with
* (clang) -Wall -Wextra -Wpedantic.
*
* To integrate it into your codebase, define RT_REPLAY_IMPLEMENTATION
* before including it in one source file.
* By default, the library depends on stdint.h (for fixed size integers),
* and on stdlib.h (for malloc & free).
* You can provide replacements for these by defining macros as described
* below.
*
* It's provided under the terms of the MIT license (found below)
*/
/* The MIT License (MIT)
* Copyright © 2023 Kevin Trogant
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the “Software”), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* Fixed size integers.
*
* You can replace these by defining the macros below.
* HOWEVER: Please note that this might break previous recordings if you
* inadvertently end up changing the size of the integers.
*/
#if !defined(RTR_INT32) || !defined(RTR_UINT32) || !defined(RTR_UINT64)
#include <stdint.h>
#endif
#if !defined(RTR_INT32)
#define RTR_INT32 int32_t
#endif
#if !defined(RTR_UINT32)
#define RTR_UINT32 uint32_t
#endif
#if !defined(RTR_UINT64)
#define RTR_UINT64 uint64_t
#endif
typedef RTR_INT32 rtr_int32;
typedef RTR_UINT32 rtr_uint32;
typedef RTR_UINT64 rtr_uint64;
/* Allocators.
*
* You can replace the allocation functions by defining RTR_MALLOC
* and RTR_FREE. Both take a void* context parameter, that is user defined
* and can be used by your functions however you need.
*/
#if !defined(RTR_MALLOC) && !defined(RTR_FREE)
#include <stdlib.h>
#define RTR_MALLOC(ctx, sz) malloc(sz)
#define RTR_FREE(ctx, ptr) free(ptr)
#elif !defined(RTR_MALLOC) || !defined(RTR_FREE)
#error "You must define RTR_MALLOC and RTR_FREE or none"
#endif
/* Memcpy & memset
*
* You can replace these by defining the macros below.
*/
#if !defined(RTR_MEMCPY) || !defined(RTR_MEMSET)
#include <string.h>
#endif
#if !defined(RTR_MEMCPY)
#define RTR_MEMCPY(dst, src, n) memcpy((void*)(dst), (const void*)(src), (size_t)(n))
#endif
#if !defined(RTR_MEMSET)
#define RTR_MEMSET(dst, val, n) memset((void*)(dst), (int)(val), (size_t)(n))
#endif
enum
{
RTR_SUCCESS,
RTR_ERROR_OUT_OF_MEMORY,
RTR_ERROR_NO_FRAME,
RTR_ERROR_INVALID_RECORDING,
};
enum
{
RTR_RECORDING_MODE_RECORD,
RTR_RECORDING_MODE_PLAYBACK,
};
enum
{
RTR_EVENT_TYPE_KEY,
RTR_EVENT_TYPE_ANALOG,
};
typedef struct rtr_record_frame rtr_record_frame;
/* A recording keeps a list of record frames, where
* each frame contains a timestamp and a number of input events.
*
* During replay, events are fed back to the game once the timestamp is reached.
*/
typedef struct
{
unsigned int mode;
double timestamp;
rtr_record_frame* frames;
rtr_record_frame* last_frame;
rtr_uint32 num_frames;
void* ctx;
} rtr_recording;
typedef struct
{
rtr_uint32 type;
union
{
struct
{
rtr_uint32 keycode;
rtr_uint32 state;
} key;
struct
{
rtr_uint32 axis;
double value;
} analog;
} data;
} rtr_event;
typedef struct
{
rtr_record_frame* frame;
double timestamp;
int is_finished;
} rtr_playback_cursor;
/*
* Recording api
*/
void rtr_begin_recording(void* user_ctx, rtr_recording* recording);
int rtr_end_recording(rtr_recording* recording, void** buffer, rtr_uint64* buffer_size);
int rtr_frame(rtr_recording* recording, double delta);
int rtr_add_key_event(rtr_recording* recording, rtr_uint32 key, rtr_uint32 state);
int rtr_add_analog_event(rtr_recording* recording, rtr_uint32 axis, double value);
/*
* Playback api
*/
int rtr_open_recording(void* buffer, rtr_uint64 buffer_size, void* user_ctx, rtr_recording* recording, rtr_playback_cursor* cursor);
void rtr_close_recording(rtr_recording* recording);
rtr_playback_cursor rtr_create_cursor(rtr_recording* recording);
void rtr_advance_playback(rtr_playback_cursor* cursor, double delta);
int rtr_get_events(rtr_playback_cursor* cursor, unsigned int* num_events, rtr_event* events);
#endif
#ifdef RT_REPLAY_IMPLEMENTATION
#undef RT_REPLAY_IMPLEMENTATION
#include <stddef.h>
#define RTR_UNUSED(x) (void)(sizeof(x))
#define RTR_EVENTS_PER_FRAME 256
struct rtr_record_frame
{
double timestamp;
rtr_record_frame* next;
rtr_uint32 num_events;
rtr_event events[RTR_EVENTS_PER_FRAME];
};
#define RTR_MAKE_VERSION(major, minor, patch) (\
(((major) & 0xfff) << 20) | \
(((minor) & 0xfff) << 8) | \
((patch) & 0xff))
#define RTR_VERSION RTR_MAKE_VERSION(0, 1, 0)
typedef struct
{
rtr_uint32 version;
rtr_uint32 num_frames;
} rtr_buffer_header;
typedef struct
{
double rel_timestamp;
rtr_uint32 num_events;
rtr_event events[RTR_EVENTS_PER_FRAME];
} rtr_buffer_frame;
void rtr_begin_recording(void* user_ctx, rtr_recording* recording)
{
rtr_recording rec;
rec.mode = RTR_RECORDING_MODE_RECORD;
rec.timestamp = 0.0;
rec.num_frames = 0;
rec.frames = NULL;
rec.ctx = user_ctx;
*recording = rec;
}
int rtr_end_recording(rtr_recording* recording, void** buffer, rtr_uint64* buffer_size)
{
if (recording->mode != RTR_RECORDING_MODE_RECORD)
return RTR_ERROR_INVALID_RECORDING;
/* Count the frames */
rtr_int32 unseen_frames = (rtr_int32)recording->num_frames;
for (rtr_record_frame* frame = recording->frames; frame != NULL; frame = frame->next) {
--unseen_frames;
}
if (unseen_frames != 0)
return RTR_ERROR_INVALID_RECORDING;
rtr_uint64 req_size = sizeof(rtr_buffer_header) + sizeof(rtr_buffer_frame) * recording->num_frames;
char* buf = RTR_MALLOC(recording->ctx, req_size);
if (!buf)
return RTR_ERROR_OUT_OF_MEMORY;
rtr_buffer_header* hdr = (rtr_buffer_header*)buf;
hdr->version = RTR_VERSION;
hdr->num_frames = recording->num_frames;
rtr_buffer_frame* buf_frames = (rtr_buffer_frame*)(hdr + 1);
int i = 0;
rtr_record_frame* frame = recording->frames;
while (frame) {
buf_frames[i].rel_timestamp = frame->timestamp;
buf_frames[i].num_events = frame->num_events;
RTR_MEMCPY(buf_frames[i].events, frame->events, sizeof(rtr_event) * frame->num_events);
rtr_record_frame* tmp = frame;
frame = frame->next;
RTR_FREE(recording->ctx, tmp);
}
RTR_MEMSET(recording, 0, sizeof(recording));
*buffer = (void*)buf;
*buffer_size = req_size;
return RTR_SUCCESS;
}
int rtr_frame(rtr_recording* recording, double delta)
{
rtr_record_frame* frame = (rtr_record_frame*)RTR_MALLOC(recording->ctx, sizeof(rtr_record_frame));
if (!frame) {
return RTR_ERROR_OUT_OF_MEMORY;
}
recording->timestamp += delta;
frame->num_events = 0;
frame->next = NULL;
frame->timestamp = recording->timestamp;
if (recording->last_frame)
recording->last_frame->next = frame;
recording->last_frame = frame;
++recording->num_frames;
return RTR_SUCCESS;
}
static int rtr_add_event(rtr_recording* recording, rtr_event event)
{
if (!recording->last_frame)
return RTR_ERROR_NO_FRAME;
rtr_record_frame* frame = recording->last_frame;
if (frame->num_events < RTR_EVENTS_PER_FRAME) {
frame->events[frame->num_events++] = event;
return RTR_SUCCESS;
}
else {
/* add a new "extended" frame with the same timestamp */
rtr_record_frame* ext_frame = (rtr_record_frame*)RTR_MALLOC(recording->ctx, sizeof(rtr_record_frame));
if (!ext_frame)
return RTR_ERROR_OUT_OF_MEMORY;
ext_frame->timestamp = frame->timestamp;
ext_frame->next = NULL;
ext_frame->events[0] = event;
ext_frame->num_events = 1;
frame->next = ext_frame;
recording->last_frame = ext_frame;
return RTR_SUCCESS;
}
}
int rtr_add_key_event(rtr_recording* recording, rtr_uint32 key, rtr_uint32 state)
{
rtr_event event;
event.type = RTR_EVENT_TYPE_KEY;
event.data.key.keycode = key;
event.data.key.state = state;
return rtr_add_event(recording, event);
}
int rtr_add_analog_event(rtr_recording* recording, rtr_uint32 axis, double value)
{
rtr_event event;
event.type = RTR_EVENT_TYPE_ANALOG;
event.data.analog.axis = axis;
event.data.analog.value = value;
return rtr_add_event(recording, event);
}
int rtr_open_recording(void* buffer, rtr_uint64 buffer_size, void* user_ctx, rtr_recording* recording, rtr_playback_cursor* cursor)
{
rtr_buffer_header* hdr = (rtr_buffer_header*)buffer;
if (hdr->version != RTR_VERSION) {
/* because this is the only valid version, this check suffices.
* in the future, we would check if the buffer contains a version
* we recognize */
return RTR_ERROR_INVALID_RECORDING;
}
if (((hdr->num_frames * sizeof(rtr_buffer_frame)) + sizeof(hdr)) > buffer_size) {
/* something went wrong */
return RTR_ERROR_INVALID_RECORDING;
}
rtr_buffer_frame* buf_frames = (rtr_buffer_frame*)(hdr + 1);
rtr_record_frame* frames = RTR_MALLOC(user_ctx, sizeof(rtr_record_frame) * hdr->num_frames);
for (unsigned int i = 0; i < hdr->num_frames; ++i) {
frames[i].next = (i < hdr->num_frames - 1) ? &(frames[i + 1]) : NULL;
frames[i].num_events = buf_frames[i].num_events;
frames[i].timestamp = buf_frames[i].rel_timestamp;
RTR_MEMCPY(frames[i].events, buf_frames[i].events, buf_frames[i].num_events * sizeof(rtr_event));
}
recording->frames = frames;
recording->last_frame = &frames[hdr->num_frames - 1];
recording->mode = RTR_RECORDING_MODE_PLAYBACK;
recording->num_frames = hdr->num_frames;
recording->timestamp = 0.0;
recording->ctx = user_ctx;
if (cursor) {
cursor->frame = recording->frames;
cursor->is_finished = 0;
cursor->timestamp = 0.0;
}
return RTR_SUCCESS;
}
void rtr_close_recording(rtr_recording* recording)
{
if (recording->mode != RTR_RECORDING_MODE_PLAYBACK)
return;
RTR_FREE(recording->ctx, recording->frames);
RTR_MEMSET(recording, 0, sizeof(recording));
}
rtr_playback_cursor rtr_create_cursor(rtr_recording* recording)
{
rtr_playback_cursor cursor = {NULL, 0.0, 0};
if (recording->mode != RTR_RECORDING_MODE_PLAYBACK)
return cursor;
cursor.frame = recording->frames;
cursor.is_finished = 0;
cursor.timestamp = 0.0;
return cursor;
}
void rtr_advance_playback(rtr_playback_cursor* cursor, double delta)
{
rtr_record_frame* next_frame = cursor->frame->next;
/* skip extended frames */
while (next_frame && next_frame->timestamp == cursor->frame->timestamp)
next_frame = next_frame->next;
if (next_frame) {
cursor->timestamp += delta;
if (cursor->timestamp >= next_frame->timestamp)
cursor->frame = next_frame;
}
else {
cursor->is_finished = 1;
}
}
int rtr_get_events(rtr_playback_cursor* cursor, unsigned int* num_events, rtr_event* events)
{
/* its possible that multiple frames are concatenated together */
unsigned int n = (unsigned int)cursor->frame->num_events;
double ts = cursor->frame->timestamp;
rtr_record_frame* ext_frame = cursor->frame->next;
while (ext_frame->timestamp == ts) {
n += ext_frame->num_events;
ext_frame = ext_frame->next;
}
if (num_events)
*num_events = n;
if (events) {
RTR_MEMCPY(events, cursor->frame->events, cursor->frame->num_events * sizeof(rtr_event));
rtr_uint32 i = cursor->frame->num_events;
rtr_record_frame* ext_frame = cursor->frame->next;
while (ext_frame->timestamp == ts) {
RTR_MEMCPY(&events[i], ext_frame->events, ext_frame->num_events * sizeof(rtr_event));
i += ext_frame->num_events;
ext_frame = ext_frame->next;
}
}
return 1;
}
#endif