Merge branch 'master' into dev

This commit is contained in:
bjorn 2022-12-16 21:21:05 -08:00
commit e12563ad25
25 changed files with 522 additions and 325 deletions

View File

@ -15,6 +15,7 @@ option(LOVR_ENABLE_SYSTEM "Enable the system module" ON)
option(LOVR_ENABLE_THREAD "Enable the thread module" ON)
option(LOVR_ENABLE_TIMER "Enable the timer module" ON)
option(LOVR_USE_GLFW "Use GLFW for desktop windows" ON)
option(LOVR_USE_LUAJIT "Use LuaJIT instead of Lua" ON)
option(LOVR_USE_GLSLANG "Use glslang to compile GLSL shaders" ON)
option(LOVR_USE_VULKAN "Use the Vulkan renderer" ON)
@ -78,7 +79,7 @@ if(NOT ANDROID AND LOVR_BUILD_SHARED)
endif()
# GLFW
if(NOT (EMSCRIPTEN OR ANDROID))
if(LOVR_USE_GLFW AND NOT (EMSCRIPTEN OR ANDROID))
if(LOVR_SYSTEM_GLFW)
pkg_search_module(GLFW REQUIRED glfw3)
include_directories(${GLFW_INCLUDE_DIRS})
@ -569,13 +570,16 @@ if(LOVR_ENABLE_SYSTEM)
src/modules/system/system.c
src/api/l_system.c
)
if(LOVR_USE_GLFW)
target_compile_definitions(lovr PRIVATE LOVR_USE_GLFW)
endif()
else()
target_compile_definitions(lovr PRIVATE LOVR_DISABLE_SYSTEM)
endif()
if(LOVR_ENABLE_THREAD)
target_sources(lovr PRIVATE
src/modules/thread/channel.c
src/modules/thread/thread.c
src/api/l_thread.c
src/api/l_thread_channel.c

View File

@ -96,8 +96,7 @@ Resources
- [**Documentation**](https://lovr.org/docs): Guides, tutorials, examples, and API documentation.
- [**FAQ**](https://lovr.org/docs/FAQ): Frequently Asked Questions.
- [**Slack Group**](https://lovr.org/slack): For general LÖVR discussion and support.
- [**Matrix Room**](https://matrix.to/#/!XVAslexgYDYQnYnZBP:matrix.org): Decentralized alternative to Slack.
- [**Matrix**](https://lovr.org/matrix): The LÖVR community for discussion and support.
- [**Nightly Builds**](https://lovr.org/download/nightly): Nightly builds for Windows.
- [**Compiling Guide**](https://lovr.org/docs/Compiling): Information on compiling LÖVR from source.
- [**Contributing**](https://lovr.org/docs/Contributing): Guide for helping out with development 💜

View File

@ -5,6 +5,7 @@ config = {
supercharge = false,
sanitize = false,
strict = true,
glfw = true,
luajit = false,
glslang = true,
modules = {
@ -221,8 +222,9 @@ else
tup.rule('.obj/lua/*.o', '^ LD %o^ $(cc) $(flags) -o %o %f $(lua_lflags)', lib('lua'))
end
if target == 'win32' or target == 'macos' or target == 'linux' then
if config.glfw and (target == 'win32' or target == 'macos' or target == 'linux') then
cflags += '-Ideps/glfw/include'
cflags += '-DLOVR_USE_GLFW'
lflags += '-lglfw'
glfw_cflags += '-fPIC'

View File

@ -462,7 +462,8 @@ void luax_optcolor(lua_State* L, int index, float color[4]) {
color[3] = luax_optfloat(L, -1, 1.);
lua_pop(L, 4);
break;
case LUA_TUSERDATA: {
case LUA_TUSERDATA:
case LUA_TLIGHTUSERDATA: {
VectorType type;
float* v = luax_tovector(L, index, &type);
if (type == V_VEC3) {
@ -473,8 +474,7 @@ void luax_optcolor(lua_State* L, int index, float color[4]) {
memcpy(color, v, 4 * sizeof(float));
break;
}
/* fallthrough */
}
} /* fallthrough */
default: lovrThrow("Expected nil, number, table, vec3, or vec4 for color value");
}
}

View File

@ -141,6 +141,7 @@ static int nextEvent(lua_State* L) {
luax_pushtype(L, Thread, event.data.thread.thread);
lua_pushstring(L, event.data.thread.error);
lovrRelease(event.data.thread.thread, lovrThreadDestroy);
free(event.data.thread.error);
return 3;
#endif

View File

@ -494,7 +494,7 @@ static Canvas luax_checkcanvas(lua_State* L, int index) {
case LUA_TUSERDATA: canvas.depth.texture = luax_checktype(L, -1, Texture); break;
case LUA_TTABLE:
lua_getfield(L, -1, "format");
canvas.depth.format = lua_isnil(L, -1) ? canvas.depth.format : luax_checkenum(L, -1, TextureFormat, NULL);
canvas.depth.format = lua_isnil(L, -1) ? canvas.depth.format : (uint32_t) luax_checkenum(L, -1, TextureFormat, NULL);
lua_pop(L, 1);
lua_getfield(L, -1, "texture");
@ -981,12 +981,12 @@ static int l_lovrGraphicsNewTexture(lua_State* L) {
if (lua_istable(L, index)) {
lua_getfield(L, index, "type");
info.type = lua_isnil(L, -1) ? info.type : luax_checkenum(L, -1, TextureType, NULL);
info.type = lua_isnil(L, -1) ? info.type : (uint32_t) luax_checkenum(L, -1, TextureType, NULL);
lua_pop(L, 1);
if (info.imageCount == 0) {
lua_getfield(L, index, "format");
info.format = lua_isnil(L, -1) ? info.format : luax_checkenum(L, -1, TextureFormat, NULL);
info.format = lua_isnil(L, -1) ? info.format : (uint32_t) luax_checkenum(L, -1, TextureFormat, NULL);
lua_pop(L, 1);
lua_getfield(L, index, "samples");
@ -1428,6 +1428,7 @@ static int l_lovrGraphicsNewFont(lua_State* L) {
lovrRelease(blob, lovrBlobDestroy);
} else {
info.spread = luaL_optnumber(L, 2, info.spread);
lovrRetain(info.rasterizer);
}
Font* font = lovrFontCreate(&info);

View File

@ -555,12 +555,13 @@ static void luax_readvertices(lua_State* L, int index, float* vertices, uint32_t
*vertices++ = luax_tofloat(L, -1);
lua_pop(L, 1);
}
} else if (innerType == LUA_TUSERDATA) {
} else if (innerType == LUA_TUSERDATA || innerType == LUA_TLIGHTUSERDATA) {
for (uint32_t i = 0; i < count; i++) {
lua_rawgeti(L, index, i + 1);
vec3_init(vertices, luax_checkvector(L, -1, V_VEC3, NULL));
lua_pop(L, 1);
float* v = luax_checkvector(L, -1, V_VEC3, NULL);
memcpy(vertices, v, 3 * sizeof(float));
vertices += 3;
lua_pop(L, 1);
}
}
break;
@ -644,7 +645,7 @@ static int l_lovrPassSphere(lua_State* L) {
return 0;
}
static bool luax_checkendpoints(lua_State* L, int index, float transform[16]) {
static bool luax_checkendpoints(lua_State* L, int index, float transform[16], bool center) {
float *v, *u;
VectorType t1, t2;
if ((v = luax_tovector(L, index + 0, &t1)) == NULL || t1 != V_VEC3) return false;
@ -658,7 +659,11 @@ static bool luax_checkendpoints(lua_State* L, int index, float transform[16]) {
vec3_normalize(direction);
quat_between(orientation, forward, direction);
mat4_identity(transform);
mat4_translate(transform, (v[0] + u[0]) / 2.f, (v[1] + u[1]) / 2.f, (v[2] + u[2]) / 2.f);
if (center) {
mat4_translate(transform, (v[0] + u[0]) / 2.f, (v[1] + u[1]) / 2.f, (v[2] + u[2]) / 2.f);
} else {
mat4_translate(transform, v[0], v[1], v[2]);
}
mat4_rotateQuat(transform, orientation);
mat4_scale(transform, radius, radius, length);
return true;
@ -667,7 +672,7 @@ static bool luax_checkendpoints(lua_State* L, int index, float transform[16]) {
static int l_lovrPassCylinder(lua_State* L) {
Pass* pass = luax_checktype(L, 1, Pass);
float transform[16];
int index = luax_checkendpoints(L, 2, transform) ? 5 : luax_readmat4(L, 2, transform, -2);
int index = luax_checkendpoints(L, 2, transform, true) ? 5 : luax_readmat4(L, 2, transform, -2);
bool capped = lua_isnoneornil(L, index) ? true : lua_toboolean(L, index++);
float angle1 = luax_optfloat(L, index++, 0.f);
float angle2 = luax_optfloat(L, index++, 2.f * (float) M_PI);
@ -679,7 +684,7 @@ static int l_lovrPassCylinder(lua_State* L) {
static int l_lovrPassCone(lua_State* L) {
Pass* pass = luax_checktype(L, 1, Pass);
float transform[16];
int index = luax_readmat4(L, 2, transform, -2);
int index = luax_checkendpoints(L, 2, transform, false) ? 5 : luax_readmat4(L, 2, transform, -2);
uint32_t segments = luax_optu32(L, index, 64);
lovrPassCone(pass, transform, segments);
return 0;
@ -688,7 +693,7 @@ static int l_lovrPassCone(lua_State* L) {
static int l_lovrPassCapsule(lua_State* L) {
Pass* pass = luax_checktype(L, 1, Pass);
float transform[16];
int index = luax_checkendpoints(L, 2, transform) ? 5 : luax_readmat4(L, 2, transform, -2);
int index = luax_checkendpoints(L, 2, transform, true) ? 5 : luax_readmat4(L, 2, transform, -2);
uint32_t segments = luax_optu32(L, index, 32);
lovrPassCapsule(pass, transform, segments);
return 0;

View File

@ -1,20 +1,13 @@
#include "api.h"
#include "data/blob.h"
#include "event/event.h"
#include "thread/thread.h"
#include "thread/channel.h"
#include "util.h"
#include <lualib.h>
#include <stdlib.h>
#include <string.h>
static int threadRunner(void* data) {
Thread* thread = (Thread*) data;
lovrRetain(thread);
mtx_lock(&thread->lock);
thread->running = true;
mtx_unlock(&thread->lock);
static char* threadRunner(Thread* thread, Blob* body, Variant* arguments, uint32_t argumentCount) {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
luax_preload(L);
@ -23,42 +16,29 @@ static int threadRunner(void* data) {
lua_pushcfunction(L, luax_getstack);
int errhandler = lua_gettop(L);
if (!luaL_loadbuffer(L, thread->body->data, thread->body->size, "thread")) {
for (uint32_t i = 0; i < thread->argumentCount; i++) {
luax_pushvariant(L, &thread->arguments[i]);
if (!luaL_loadbuffer(L, body->data, body->size, "thread")) {
for (uint32_t i = 0; i < argumentCount; i++) {
luax_pushvariant(L, &arguments[i]);
}
if (!lua_pcall(L, thread->argumentCount, 0, errhandler)) {
mtx_lock(&thread->lock);
thread->running = false;
mtx_unlock(&thread->lock);
lovrRelease(thread, lovrThreadDestroy);
lua_close(L);
return 0;
if (!lua_pcall(L, argumentCount, 0, errhandler)) {
return NULL;
}
}
mtx_lock(&thread->lock);
// Error handling
size_t length;
const char* error = lua_tolstring(L, -1, &length);
const char* message = lua_tolstring(L, -1, &length);
char* error = message ? malloc(length + 1) : NULL;
if (error) {
thread->error = malloc(length + 1);
if (thread->error) {
memcpy(thread->error, error, length + 1);
lovrEventPush((Event) {
.type = EVENT_THREAD_ERROR,
.data.thread = { thread, thread->error }
});
}
memcpy(error, message, length + 1);
lua_close(L);
return error;
}
thread->running = false;
mtx_unlock(&thread->lock);
lovrRelease(thread, lovrThreadDestroy);
lua_close(L);
return 1;
return NULL;
}
static int l_lovrThreadNewThread(lua_State* L) {

View File

@ -1,5 +1,5 @@
#include "api.h"
#include "thread/channel.h"
#include "thread/thread.h"
#include "event/event.h"
#include "util.h"
#include <math.h>

View File

@ -1,4 +1,5 @@
#include "api.h"
#include "event/event.h"
#include "thread/thread.h"
#include "util.h"
@ -32,7 +33,8 @@ static int l_lovrThreadGetError(lua_State* L) {
static int l_lovrThreadIsRunning(lua_State* L) {
Thread* thread = luax_checktype(L, 1, Thread);
lua_pushboolean(L, thread->running);
bool running = lovrThreadIsRunning(thread);
lua_pushboolean(L, running);
return 1;
}

View File

@ -2049,15 +2049,15 @@ bool gpu_init(gpu_config* config) {
enable->largePoints = supports->largePoints;
// Optional features (currently always enabled when supported)
config->features->textureBC = enable->textureCompressionBC = supports->textureCompressionBC;
config->features->textureASTC = enable->textureCompressionASTC_LDR = supports->textureCompressionASTC_LDR;
config->features->wireframe = enable->fillModeNonSolid = supports->fillModeNonSolid;
config->features->depthClamp = enable->depthClamp = supports->depthClamp;
config->features->indirectDrawFirstInstance = enable->drawIndirectFirstInstance = supports->drawIndirectFirstInstance;
config->features->shaderTally = enable->pipelineStatisticsQuery = supports->pipelineStatisticsQuery;
config->features->float64 = enable->shaderFloat64 = supports->shaderFloat64;
config->features->int64 = enable->shaderInt64 = supports->shaderInt64;
config->features->int16 = enable->shaderInt16 = supports->shaderInt16;
config->features->textureBC = (enable->textureCompressionBC = supports->textureCompressionBC);
config->features->textureASTC = (enable->textureCompressionASTC_LDR = supports->textureCompressionASTC_LDR);
config->features->wireframe = (enable->fillModeNonSolid = supports->fillModeNonSolid);
config->features->depthClamp = (enable->depthClamp = supports->depthClamp);
config->features->indirectDrawFirstInstance = (enable->drawIndirectFirstInstance = supports->drawIndirectFirstInstance);
config->features->shaderTally = (enable->pipelineStatisticsQuery = supports->pipelineStatisticsQuery);
config->features->float64 = (enable->shaderFloat64 = supports->shaderFloat64);
config->features->int64 = (enable->shaderInt64 = supports->shaderInt64);
config->features->int16 = (enable->shaderInt16 = supports->shaderInt16);
// Formats
for (uint32_t i = 0; i < GPU_FORMAT_COUNT; i++) {
@ -3070,7 +3070,9 @@ static void nickname(void* handle, VkObjectType type, const char* name) {
static bool vcheck(VkResult result, const char* message) {
if (result >= 0) return true;
if (!state.config.callback) return false;
#define CASE(x) case x: state.config.callback(state.config.userdata, "Vulkan error: " #x, true); break;
const char* errorCode = "";
#define CASE(x) case x: errorCode = " (" #x ")"; break;
switch (result) {
CASE(VK_ERROR_OUT_OF_HOST_MEMORY);
CASE(VK_ERROR_OUT_OF_DEVICE_MEMORY);
@ -3088,8 +3090,21 @@ static bool vcheck(VkResult result, const char* message) {
default: break;
}
#undef CASE
state.config.callback(state.config.userdata, message, true);
return false;
char string[128];
size_t length1 = strlen(message);
size_t length2 = strlen(errorCode);
if (length1 + length2 >= sizeof(string)) {
state.config.callback(state.config.userdata, message, true);
return false;
} else {
memcpy(string, message, length1);
memcpy(string + length1, errorCode, length2);
string[length1 + length2] = '\0';
state.config.callback(state.config.userdata, string, true);
return false;
}
}
static bool check(bool condition, const char* message) {

View File

@ -170,6 +170,22 @@ bool os_init() {
}
void os_destroy() {
// There are two ways a quit can happen, which need to be handled slightly differently:
// - If the system tells us to quit, we get an event with APP_CMD_DESTROY. In response we push a
// QUIT event to lovr.event and main will eventually exit cleanly. No other teardown necessary.
// - If the app exits manually (e.g. lovr.event.quit), then Android thinks we're still running and
// the app is still in APP_CMD_RESUME. We need to tell it to exit with ANativeActivity_Finish
// and poll for events until the app state changes, otherwise the app is left in a broken state.
if (state.app->activityState == APP_CMD_RESUME) {
state.onQuit = NULL;
state.onKeyboardEvent = NULL;
state.onTextEvent = NULL;
state.onPermissionEvent = NULL;
ANativeActivity_finish(state.app->activity);
while (!state.app->destroyRequested) {
os_poll_events();
}
}
memset(&state, 0, sizeof(state));
}

View File

@ -1,3 +1,77 @@
#ifndef LOVR_USE_GLFW
void os_destroy() {
//
}
void os_poll_events() {
//
}
bool os_window_open(const os_window_config* config) {
return false;
}
bool os_window_is_open() {
return false;
}
void os_window_get_size(uint32_t* width, uint32_t* height) {
*width = *height = 0;
}
void os_window_get_fbsize(uint32_t* width, uint32_t* height) {
*width = *height = 0;
}
void os_on_quit(fn_quit* callback) {
//
}
void os_on_focus(fn_focus* callback) {
//
}
void os_on_resize(fn_resize* callback) {
//
}
void os_on_key(fn_key* callback) {
//
}
void os_on_text(fn_text* callback) {
//
}
void os_get_mouse_position(double* x, double* y) {
*x = *y = 0.;
}
void os_set_mouse_mode(os_mouse_mode mode) {
//
}
bool os_is_mouse_down(os_mouse_button button) {
return false;
}
bool os_is_key_down(os_key key) {
return false;
}
#ifdef LOVR_VK
const char** os_vk_get_instance_extensions(uint32_t* count) {
return *count = 0, NULL;
}
uint32_t os_vk_create_surface(void* instance, void** surface) {
return -13; // VK_ERROR_UNKNOWN
}
#endif
#else
#include <stdio.h>
#ifdef LOVR_VK
@ -180,6 +254,10 @@ static int convertKey(os_key key) {
}
}
void os_destroy() {
glfwTerminate();
}
void os_poll_events() {
if (glfwState.window) {
glfwPollEvents();
@ -203,12 +281,7 @@ bool os_window_open(const os_window_config* config) {
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
#endif
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
glfwWindowHint(GLFW_RESIZABLE, config->resizable);
glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE);
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
@ -340,3 +413,5 @@ uint32_t os_vk_create_surface(void* instance, void** surface) {
OS_DLL_EXPORT GLFWwindow* os_get_glfw_window(void) {
return glfwState.window;
}
#endif

View File

@ -14,10 +14,6 @@ bool os_init() {
return true;
}
void os_destroy() {
glfwTerminate();
}
const char* os_get_name() {
return "Linux";
}

View File

@ -26,11 +26,6 @@ bool os_init() {
return true;
}
void os_destroy() {
glfwTerminate();
memset(&state, 0, sizeof(state));
}
const char* os_get_name() {
return "macOS";
}

View File

@ -62,10 +62,6 @@ bool os_init() {
return true;
}
void os_destroy() {
glfwTerminate();
}
const char* os_get_name() {
return "Windows";
}

View File

@ -1130,30 +1130,30 @@ static Image* loadKTX2(Blob* blob) {
case 126: image->format = FORMAT_D32F; break;
case 129: image->format = FORMAT_D24S8; break;
case 130: image->format = FORMAT_D32FS8; break;
case 132: image->flags |= IMAGE_SRGB; case 131: image->format = FORMAT_BC1; break;
case 136: image->flags |= IMAGE_SRGB; case 135: image->format = FORMAT_BC2; break;
case 138: image->flags |= IMAGE_SRGB; case 137: image->format = FORMAT_BC3; break;
case 132: image->flags |= IMAGE_SRGB; /* fallthrough */ case 131: image->format = FORMAT_BC1; break;
case 136: image->flags |= IMAGE_SRGB; /* fallthrough */ case 135: image->format = FORMAT_BC2; break;
case 138: image->flags |= IMAGE_SRGB; /* fallthrough */ case 137: image->format = FORMAT_BC3; break;
case 139: image->format = FORMAT_BC4U; break;
case 140: image->format = FORMAT_BC4S; break;
case 141: image->format = FORMAT_BC5U; break;
case 142: image->format = FORMAT_BC5S; break;
case 143: image->format = FORMAT_BC6UF; break;
case 144: image->format = FORMAT_BC6SF; break;
case 146: image->flags |= IMAGE_SRGB; case 145: image->format = FORMAT_BC7; break;
case 158: image->flags |= IMAGE_SRGB; case 157: image->format = FORMAT_ASTC_4x4; break;
case 160: image->flags |= IMAGE_SRGB; case 159: image->format = FORMAT_ASTC_5x4; break;
case 162: image->flags |= IMAGE_SRGB; case 161: image->format = FORMAT_ASTC_5x5; break;
case 164: image->flags |= IMAGE_SRGB; case 163: image->format = FORMAT_ASTC_6x5; break;
case 166: image->flags |= IMAGE_SRGB; case 165: image->format = FORMAT_ASTC_6x6; break;
case 168: image->flags |= IMAGE_SRGB; case 167: image->format = FORMAT_ASTC_8x5; break;
case 170: image->flags |= IMAGE_SRGB; case 169: image->format = FORMAT_ASTC_8x6; break;
case 172: image->flags |= IMAGE_SRGB; case 171: image->format = FORMAT_ASTC_8x8; break;
case 174: image->flags |= IMAGE_SRGB; case 173: image->format = FORMAT_ASTC_10x5; break;
case 176: image->flags |= IMAGE_SRGB; case 175: image->format = FORMAT_ASTC_10x6; break;
case 178: image->flags |= IMAGE_SRGB; case 177: image->format = FORMAT_ASTC_10x8; break;
case 180: image->flags |= IMAGE_SRGB; case 179: image->format = FORMAT_ASTC_10x10; break;
case 182: image->flags |= IMAGE_SRGB; case 181: image->format = FORMAT_ASTC_12x10; break;
case 184: image->flags |= IMAGE_SRGB; case 183: image->format = FORMAT_ASTC_12x12; break;
case 146: image->flags |= IMAGE_SRGB; /* fallthrough */ case 145: image->format = FORMAT_BC7; break;
case 158: image->flags |= IMAGE_SRGB; /* fallthrough */ case 157: image->format = FORMAT_ASTC_4x4; break;
case 160: image->flags |= IMAGE_SRGB; /* fallthrough */ case 159: image->format = FORMAT_ASTC_5x4; break;
case 162: image->flags |= IMAGE_SRGB; /* fallthrough */ case 161: image->format = FORMAT_ASTC_5x5; break;
case 164: image->flags |= IMAGE_SRGB; /* fallthrough */ case 163: image->format = FORMAT_ASTC_6x5; break;
case 166: image->flags |= IMAGE_SRGB; /* fallthrough */ case 165: image->format = FORMAT_ASTC_6x6; break;
case 168: image->flags |= IMAGE_SRGB; /* fallthrough */ case 167: image->format = FORMAT_ASTC_8x5; break;
case 170: image->flags |= IMAGE_SRGB; /* fallthrough */ case 169: image->format = FORMAT_ASTC_8x6; break;
case 172: image->flags |= IMAGE_SRGB; /* fallthrough */ case 171: image->format = FORMAT_ASTC_8x8; break;
case 174: image->flags |= IMAGE_SRGB; /* fallthrough */ case 173: image->format = FORMAT_ASTC_10x5; break;
case 176: image->flags |= IMAGE_SRGB; /* fallthrough */ case 175: image->format = FORMAT_ASTC_10x6; break;
case 178: image->flags |= IMAGE_SRGB; /* fallthrough */ case 177: image->format = FORMAT_ASTC_10x8; break;
case 180: image->flags |= IMAGE_SRGB; /* fallthrough */ case 179: image->format = FORMAT_ASTC_10x10; break;
case 182: image->flags |= IMAGE_SRGB; /* fallthrough */ case 181: image->format = FORMAT_ASTC_12x10; break;
case 184: image->flags |= IMAGE_SRGB; /* fallthrough */ case 183: image->format = FORMAT_ASTC_12x12; break;
default: lovrThrow("KTX file uses an unsupported image format");
}

View File

@ -53,6 +53,12 @@ void lovrEventPush(Event event) {
#ifndef LOVR_DISABLE_THREAD
if (event.type == EVENT_THREAD_ERROR) {
lovrRetain(event.data.thread.thread);
size_t length = strlen(event.data.thread.error);
char* copy = malloc(length + 1);
lovrAssert(copy, "Out of memory");
memcpy(copy, event.data.thread.error, length);
copy[length] = '\0';
event.data.thread.error = copy;
}
#endif

View File

@ -441,42 +441,101 @@ void lovrFilesystemSetRequirePath(const char* requirePath) {
// Archive: dir
static bool dir_resolve(char* buffer, Archive* archive, const char* path) {
char innerBuffer[LOVR_PATH_MAX];
size_t length = strlen(path);
if (length >= sizeof(innerBuffer)) return false;
length = normalize(innerBuffer, path, length);
path = innerBuffer;
enum {
PATH_INVALID,
PATH_VIRTUAL,
PATH_PHYSICAL
};
if (archive->mountpoint) {
if (strncmp(path, strpool_resolve(&archive->strings, archive->mountpoint), archive->mountpointLength)) {
return false;
} else {
path += archive->mountpointLength;
length -= archive->mountpointLength;
static int dir_resolve(Archive* archive, char* buffer, const char* path) {
char normalized[LOVR_PATH_MAX];
// Normalize the path
size_t length = strlen(path);
if (length >= sizeof(normalized)) return PATH_INVALID;
length = normalize(normalized, path, length);
// Compare each component of normalized path and mountpoint
if (archive->mountpointLength > 0) {
const char* mountpoint = strpool_resolve(&archive->strings, archive->mountpoint);
size_t mountpointLength = archive->mountpointLength;
for (;;) {
char* slash = strchr(mountpoint, '/');
size_t sublength = slash ? slash - mountpoint : mountpointLength;
// If the path is empty but there was still stuff in the mountpoint, it's a virtual directory
if (length == 0) {
// Return child directory's name for convenience in getDirectoryItems
memcpy(buffer, mountpoint, sublength);
buffer[sublength] = '\0';
return PATH_VIRTUAL;
}
// Check for paths that don't match this component of the mountpoint
if (length < sublength || strncmp(path, mountpoint, sublength)) {
return PATH_INVALID;
}
// If the path matched, make sure there's a slash after the match
if (length > sublength && path[sublength] != '/') {
return PATH_INVALID;
}
// Strip this component off of the path
if (length == sublength) {
path += sublength;
length -= sublength;
} else {
path += sublength + 1;
length -= sublength + 1;
}
// Strip this component off of the mountpoint, if mountpoint is empty then we're done
if (mountpointLength > sublength) {
mountpoint += sublength + 1;
mountpointLength -= sublength + 1;
} else {
break;
}
}
}
return concat(buffer, strpool_resolve(&archive->strings, archive->path), archive->pathLength, path, length);
// Concat archive path and normalized path (without mountpoint), return full path
if (!concat(buffer, strpool_resolve(&archive->strings, archive->path), archive->pathLength, path, length)) {
return PATH_INVALID;
}
return PATH_PHYSICAL;
}
static bool dir_stat(Archive* archive, const char* path, FileInfo* info) {
char resolved[LOVR_PATH_MAX];
return dir_resolve(resolved, archive, path) && fs_stat(resolved, info);
switch (dir_resolve(archive, resolved, path)) {
default:
case PATH_INVALID: return false;
case PATH_VIRTUAL: return fs_stat(strpool_resolve(&archive->strings, archive->path), info);
case PATH_PHYSICAL: return fs_stat(resolved, info);
}
}
static void dir_list(Archive* archive, const char* path, fs_list_cb callback, void* context) {
char resolved[LOVR_PATH_MAX];
if (dir_resolve(resolved, archive, path)) {
fs_list(resolved, callback, context);
switch (dir_resolve(archive, resolved, path)) {
case PATH_INVALID: return;
case PATH_VIRTUAL: callback(context, resolved); return;
case PATH_PHYSICAL: fs_list(resolved, callback, context); return;
}
}
static bool dir_read(Archive* archive, const char* path, size_t bytes, size_t* bytesRead, void** data) {
char resolved[LOVR_PATH_MAX];
fs_handle file;
if (dir_resolve(archive, resolved, path) != PATH_PHYSICAL) {
return false;
}
if (!dir_resolve(resolved, archive, path) || !fs_open(resolved, OPEN_READ, &file)) {
fs_handle file;
if (!fs_open(resolved, OPEN_READ, &file)) {
return false;
}
@ -531,7 +590,7 @@ static zip_node* zip_lookup(Archive* archive, const char* path) {
size_t length = strlen(path);
if (length >= sizeof(buffer)) return NULL;
length = normalize(buffer, path, length);
uint64_t hash = hash64(buffer, length);
uint64_t hash = length ? hash64(buffer, length) : 0;
uint64_t index = map_get(&archive->lookup, hash);
return index == MAP_NIL ? NULL : &archive->nodes.data[index];
}
@ -640,7 +699,10 @@ static bool zip_init(Archive* archive, const char* filename, const char* mountpo
}
mountpointLength = normalize(path, mountpoint, mountpointLength);
path[mountpointLength++] = '/';
if (mountpointLength > 0) {
path[mountpointLength++] = '/';
}
}
// Simple root normalization (only strips leading/trailing slashes, sorry)
@ -696,7 +758,7 @@ static bool zip_init(Archive* archive, const char* filename, const char* mountpo
// Keep chopping off path segments, building up a tree of paths
// We can stop early if we reach a path that has already been indexed
// Also add individual path segments to the string pool, for zip_list
while (length != SIZE_MAX) {
for (;;) {
uint64_t hash = hash64(path, length);
uint64_t index = map_get(&archive->lookup, hash);
@ -720,6 +782,16 @@ static bool zip_init(Archive* archive, const char* filename, const char* mountpo
}
archive->nodes.data[index].filename = strpool_append(&archive->strings, path + length, slash - length);
// Root node
if (length == 0) {
index = archive->nodes.length;
map_set(&archive->lookup, 0, index);
arr_push(&archive->nodes, node);
archive->nodes.data[index].filename = strpool_append(&archive->strings, path, 0);
break;
}
slash = --length;
}
}

View File

@ -418,6 +418,7 @@ static void processReadbacks(void);
static size_t getLayout(gpu_slot* slots, uint32_t count);
static gpu_bundle* getBundle(size_t layout);
static gpu_texture* getScratchTexture(gpu_texture_info* info);
static bool isDepthFormat(TextureFormat format);
static uint32_t measureTexture(TextureFormat format, uint32_t w, uint32_t h, uint32_t d);
static void checkTextureBounds(const TextureInfo* info, uint32_t offset[4], uint32_t extent[3]);
static void mipmapTexture(gpu_stream* stream, Texture* texture, uint32_t base, uint32_t count);
@ -3300,6 +3301,7 @@ Pass* lovrGraphicsGetPass(PassInfo* info) {
for (uint32_t i = 0; i < canvas->count; i++) {
const TextureInfo* texture = &canvas->textures[i]->info;
bool renderable = texture->format == GPU_FORMAT_SURFACE || (state.features.formats[texture->format] & GPU_FEATURE_RENDER);
lovrCheck(!isDepthFormat(texture->format), "Unable to use a depth texture as a color target");
lovrCheck(renderable, "This GPU does not support rendering to the texture format used by color target #%d", i + 1);
lovrCheck(texture->usage & TEXTURE_RENDER, "Texture must be created with the 'render' flag to render to it");
lovrCheck(texture->width == t->width, "Render pass texture sizes must match");
@ -3313,6 +3315,7 @@ Pass* lovrGraphicsGetPass(PassInfo* info) {
if (depth->texture || depth->format) {
TextureFormat format = depth->texture ? depth->texture->info.format : depth->format;
bool renderable = state.features.formats[format] & GPU_FEATURE_RENDER;
lovrCheck(isDepthFormat(format), "Unable to use a color texture as a depth target");
lovrCheck(renderable, "This GPU does not support depth buffers with this texture format");
if (depth->texture) {
const TextureInfo* texture = &depth->texture->info;
@ -4338,6 +4341,8 @@ void lovrPassPoints(Pass* pass, uint32_t count, float** points) {
}
void lovrPassLine(Pass* pass, uint32_t count, float** points) {
lovrCheck(count >= 2, "Need at least 2 points to make a line");
uint16_t* indices;
lovrPassDraw(pass, &(Draw) {
@ -5661,6 +5666,10 @@ static gpu_texture* getScratchTexture(gpu_texture_info* info) {
return scratch->texture;
}
static bool isDepthFormat(TextureFormat format) {
return format == FORMAT_D16 || format == FORMAT_D32F || format == FORMAT_D24S8 || format == FORMAT_D32FS8;
}
// Returns number of bytes of a 3D texture region of a given format
static uint32_t measureTexture(TextureFormat format, uint32_t w, uint32_t h, uint32_t d) {
switch (format) {

View File

@ -1,151 +0,0 @@
#include "thread/channel.h"
#include "event/event.h"
#include "util.h"
#include "lib/tinycthread/tinycthread.h"
#include <stdlib.h>
#include <stddef.h>
#include <math.h>
struct Channel {
uint32_t ref;
mtx_t lock;
cnd_t cond;
arr_t(Variant) messages;
size_t head;
uint64_t sent;
uint64_t received;
uint64_t hash;
};
Channel* lovrChannelCreate(uint64_t hash) {
Channel* channel = calloc(1, sizeof(Channel));
lovrAssert(channel, "Out of memory");
channel->ref = 1;
arr_init(&channel->messages, arr_alloc);
mtx_init(&channel->lock, mtx_plain | mtx_timed);
cnd_init(&channel->cond);
channel->hash = hash;
return channel;
}
void lovrChannelDestroy(void* ref) {
Channel* channel = ref;
lovrChannelClear(channel);
arr_free(&channel->messages);
mtx_destroy(&channel->lock);
cnd_destroy(&channel->cond);
free(channel);
}
bool lovrChannelPush(Channel* channel, Variant* variant, double timeout, uint64_t* id) {
mtx_lock(&channel->lock);
if (channel->messages.length == 0) {
lovrRetain(channel);
}
arr_push(&channel->messages, *variant);
*id = ++channel->sent;
cnd_broadcast(&channel->cond);
if (isnan(timeout) || timeout < 0) {
mtx_unlock(&channel->lock);
return false;
}
while (channel->received < *id && timeout >= 0) {
if (isinf(timeout)) {
cnd_wait(&channel->cond, &channel->lock);
} else {
struct timespec start;
struct timespec until;
struct timespec stop;
timespec_get(&start, TIME_UTC);
double whole, fraction;
fraction = modf(timeout, &whole);
until.tv_sec = start.tv_sec + whole;
until.tv_nsec = start.tv_nsec + fraction * 1e9;
cnd_timedwait(&channel->cond, &channel->lock, &until);
timespec_get(&stop, TIME_UTC);
timeout -= (stop.tv_sec - start.tv_sec) + (stop.tv_nsec - start.tv_nsec) / 1e9;
}
}
bool read = channel->received >= *id;
mtx_unlock(&channel->lock);
return read;
}
bool lovrChannelPop(Channel* channel, Variant* variant, double timeout) {
mtx_lock(&channel->lock);
do {
if (channel->head < channel->messages.length) {
*variant = channel->messages.data[channel->head++];
if (channel->head == channel->messages.length) {
channel->head = channel->messages.length = 0;
lovrRelease(channel, lovrChannelDestroy);
}
channel->received++;
cnd_broadcast(&channel->cond);
mtx_unlock(&channel->lock);
return true;
} else if (isnan(timeout) || timeout < 0) {
mtx_unlock(&channel->lock);
return false;
}
if (isinf(timeout)) {
cnd_wait(&channel->cond, &channel->lock);
} else {
struct timespec start;
struct timespec until;
struct timespec stop;
timespec_get(&start, TIME_UTC);
double whole, fraction;
fraction = modf(timeout, &whole);
until.tv_sec = start.tv_sec + whole;
until.tv_nsec = start.tv_nsec + fraction * 1e9;
cnd_timedwait(&channel->cond, &channel->lock, &until);
timespec_get(&stop, TIME_UTC);
timeout -= (stop.tv_sec - start.tv_sec) + (stop.tv_nsec - start.tv_nsec) / (double) 1e9;
}
} while (1);
}
bool lovrChannelPeek(Channel* channel, Variant* variant) {
mtx_lock(&channel->lock);
if (channel->head < channel->messages.length) {
*variant = channel->messages.data[channel->head];
mtx_unlock(&channel->lock);
return true;
}
mtx_unlock(&channel->lock);
return false;
}
void lovrChannelClear(Channel* channel) {
mtx_lock(&channel->lock);
for (size_t i = channel->head; i < channel->messages.length; i++) {
lovrVariantDestroy(&channel->messages.data[i]);
}
channel->received = channel->sent;
arr_clear(&channel->messages);
channel->head = 0;
cnd_broadcast(&channel->cond);
mtx_unlock(&channel->lock);
}
uint64_t lovrChannelGetCount(Channel* channel) {
mtx_lock(&channel->lock);
uint64_t length = channel->messages.length - channel->head;
mtx_unlock(&channel->lock);
return length;
}
bool lovrChannelHasRead(Channel* channel, uint64_t id) {
mtx_lock(&channel->lock);
bool received = channel->received >= id;
mtx_unlock(&channel->lock);
return received;
}

View File

@ -1,16 +0,0 @@
#include <stdbool.h>
#include <stdint.h>
#pragma once
struct Variant;
typedef struct Channel Channel;
Channel* lovrChannelCreate(uint64_t hash);
void lovrChannelDestroy(void* ref);
bool lovrChannelPush(Channel* channel, struct Variant* variant, double timeout, uint64_t* id);
bool lovrChannelPop(Channel* channel, struct Variant* variant, double timeout);
bool lovrChannelPeek(Channel* channel, struct Variant* variant);
void lovrChannelClear(Channel* channel);
uint64_t lovrChannelGetCount(Channel* channel);
bool lovrChannelHasRead(Channel* channel, uint64_t id);

View File

@ -1,9 +1,35 @@
#include "thread/thread.h"
#include "thread/channel.h"
#include "data/blob.h"
#include "event/event.h"
#include "util.h"
#include "lib/tinycthread/tinycthread.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
struct Thread {
uint32_t ref;
thrd_t handle;
mtx_t lock;
ThreadFunction* function;
Blob* body;
Variant arguments[MAX_THREAD_ARGUMENTS];
uint32_t argumentCount;
char* error;
bool running;
};
struct Channel {
uint32_t ref;
mtx_t lock;
cnd_t cond;
arr_t(Variant) messages;
size_t head;
uint64_t sent;
uint64_t received;
uint64_t hash;
};
static struct {
bool initialized;
mtx_t channelLock;
@ -47,12 +73,35 @@ Channel* lovrThreadGetChannel(const char* name) {
return channel;
}
Thread* lovrThreadCreate(int (*runner)(void*), Blob* body) {
// Thread
static int threadFunction(void* data) {
Thread* thread = data;
lovrRetain(thread);
char* error = thread->function(thread, thread->body, thread->arguments, thread->argumentCount);
mtx_lock(&thread->lock);
thread->running = false;
if (error) {
thread->error = error;
lovrEventPush((Event) {
.type = EVENT_THREAD_ERROR,
.data.thread = { thread, thread->error }
});
}
mtx_unlock(&thread->lock);
lovrRelease(thread, lovrThreadDestroy);
return 0;
}
Thread* lovrThreadCreate(ThreadFunction* function, Blob* body) {
Thread* thread = calloc(1, sizeof(Thread));
lovrAssert(thread, "Out of memory");
thread->ref = 1;
thread->runner = runner;
thread->body = body;
thread->function = function;
mtx_init(&thread->lock, mtx_plain);
lovrRetain(body);
return thread;
@ -61,40 +110,178 @@ Thread* lovrThreadCreate(int (*runner)(void*), Blob* body) {
void lovrThreadDestroy(void* ref) {
Thread* thread = ref;
mtx_destroy(&thread->lock);
thrd_detach(thread->handle);
if (thread->handle) thrd_detach(thread->handle);
lovrRelease(thread->body, lovrBlobDestroy);
free(thread->error);
free(thread);
}
void lovrThreadStart(Thread* thread, Variant* arguments, uint32_t argumentCount) {
bool running = lovrThreadIsRunning(thread);
if (running) {
mtx_lock(&thread->lock);
if (thread->running) {
mtx_unlock(&thread->lock);
return;
}
free(thread->error);
thread->error = NULL;
lovrAssert(argumentCount <= MAX_THREAD_ARGUMENTS, "Too many Thread arguments (max is %d)", MAX_THREAD_ARGUMENTS);
thread->argumentCount = argumentCount;
memcpy(thread->arguments, arguments, argumentCount * sizeof(Variant));
if (thrd_create(&thread->handle, thread->runner, thread) != thrd_success) {
thread->argumentCount = argumentCount;
if (thrd_create(&thread->handle, threadFunction, thread) != thrd_success) {
mtx_unlock(&thread->lock);
lovrThrow("Could not create thread...sorry");
}
thread->running = true;
mtx_unlock(&thread->lock);
}
void lovrThreadWait(Thread* thread) {
thrd_join(thread->handle, NULL);
if (thread->handle) thrd_join(thread->handle, NULL);
}
bool lovrThreadIsRunning(Thread* thread) {
mtx_lock(&thread->lock);
bool running = thread->running;
mtx_unlock(&thread->lock);
return running;
return thread->running;
}
const char* lovrThreadGetError(Thread* thread) {
return thread->error;
}
// Channel
Channel* lovrChannelCreate(uint64_t hash) {
Channel* channel = calloc(1, sizeof(Channel));
lovrAssert(channel, "Out of memory");
channel->ref = 1;
arr_init(&channel->messages, arr_alloc);
mtx_init(&channel->lock, mtx_plain | mtx_timed);
cnd_init(&channel->cond);
channel->hash = hash;
return channel;
}
void lovrChannelDestroy(void* ref) {
Channel* channel = ref;
lovrChannelClear(channel);
arr_free(&channel->messages);
mtx_destroy(&channel->lock);
cnd_destroy(&channel->cond);
free(channel);
}
bool lovrChannelPush(Channel* channel, Variant* variant, double timeout, uint64_t* id) {
mtx_lock(&channel->lock);
if (channel->messages.length == 0) {
lovrRetain(channel);
}
arr_push(&channel->messages, *variant);
*id = ++channel->sent;
cnd_broadcast(&channel->cond);
if (isnan(timeout) || timeout < 0) {
mtx_unlock(&channel->lock);
return false;
}
while (channel->received < *id && timeout >= 0) {
if (isinf(timeout)) {
cnd_wait(&channel->cond, &channel->lock);
} else {
struct timespec start;
struct timespec until;
struct timespec stop;
timespec_get(&start, TIME_UTC);
double whole, fraction;
fraction = modf(timeout, &whole);
until.tv_sec = start.tv_sec + whole;
until.tv_nsec = start.tv_nsec + fraction * 1e9;
cnd_timedwait(&channel->cond, &channel->lock, &until);
timespec_get(&stop, TIME_UTC);
timeout -= (stop.tv_sec - start.tv_sec) + (stop.tv_nsec - start.tv_nsec) / 1e9;
}
}
bool read = channel->received >= *id;
mtx_unlock(&channel->lock);
return read;
}
bool lovrChannelPop(Channel* channel, Variant* variant, double timeout) {
mtx_lock(&channel->lock);
do {
if (channel->head < channel->messages.length) {
*variant = channel->messages.data[channel->head++];
if (channel->head == channel->messages.length) {
channel->head = channel->messages.length = 0;
lovrRelease(channel, lovrChannelDestroy);
}
channel->received++;
cnd_broadcast(&channel->cond);
mtx_unlock(&channel->lock);
return true;
} else if (isnan(timeout) || timeout < 0) {
mtx_unlock(&channel->lock);
return false;
}
if (isinf(timeout)) {
cnd_wait(&channel->cond, &channel->lock);
} else {
struct timespec start;
struct timespec until;
struct timespec stop;
timespec_get(&start, TIME_UTC);
double whole, fraction;
fraction = modf(timeout, &whole);
until.tv_sec = start.tv_sec + whole;
until.tv_nsec = start.tv_nsec + fraction * 1e9;
cnd_timedwait(&channel->cond, &channel->lock, &until);
timespec_get(&stop, TIME_UTC);
timeout -= (stop.tv_sec - start.tv_sec) + (stop.tv_nsec - start.tv_nsec) / (double) 1e9;
}
} while (1);
}
bool lovrChannelPeek(Channel* channel, Variant* variant) {
mtx_lock(&channel->lock);
if (channel->head < channel->messages.length) {
*variant = channel->messages.data[channel->head];
mtx_unlock(&channel->lock);
return true;
}
mtx_unlock(&channel->lock);
return false;
}
void lovrChannelClear(Channel* channel) {
mtx_lock(&channel->lock);
for (size_t i = channel->head; i < channel->messages.length; i++) {
lovrVariantDestroy(&channel->messages.data[i]);
}
channel->received = channel->sent;
arr_clear(&channel->messages);
channel->head = 0;
cnd_broadcast(&channel->cond);
mtx_unlock(&channel->lock);
}
uint64_t lovrChannelGetCount(Channel* channel) {
mtx_lock(&channel->lock);
uint64_t length = channel->messages.length - channel->head;
mtx_unlock(&channel->lock);
return length;
}
bool lovrChannelHasRead(Channel* channel, uint64_t id) {
mtx_lock(&channel->lock);
bool received = channel->received >= id;
mtx_unlock(&channel->lock);
return received;
}

View File

@ -1,6 +1,3 @@
#include "data/blob.h"
#include "event/event.h"
#include "lib/tinycthread/tinycthread.h"
#include <stdbool.h>
#include <stdint.h>
@ -11,28 +8,34 @@
#define MAX_THREAD_ARGUMENTS 4
struct Channel;
struct Blob;
struct Variant;
typedef struct Thread {
uint32_t ref;
thrd_t handle;
mtx_t lock;
Blob* body;
Variant arguments[MAX_THREAD_ARGUMENTS];
uint32_t argumentCount;
int (*runner)(void*);
char* error;
bool running;
} Thread;
typedef struct Thread Thread;
typedef struct Channel Channel;
bool lovrThreadModuleInit(void);
void lovrThreadModuleDestroy(void);
struct Channel* lovrThreadGetChannel(const char* name);
void lovrThreadRemoveChannel(uint64_t hash);
Thread* lovrThreadCreate(int (*runner)(void*), Blob* body);
// Thread
typedef char* ThreadFunction(Thread* thread, struct Blob* body, struct Variant* arguments, uint32_t argumentCount);
Thread* lovrThreadCreate(ThreadFunction* function, struct Blob* body);
void lovrThreadDestroy(void* ref);
void lovrThreadStart(Thread* thread, Variant* arguments, uint32_t argumentCount);
void lovrThreadStart(Thread* thread, struct Variant* arguments, uint32_t argumentCount);
void lovrThreadWait(Thread* thread);
const char* lovrThreadGetError(Thread* thread);
bool lovrThreadIsRunning(Thread* thread);
const char* lovrThreadGetError(Thread* thread);
// Channel
Channel* lovrChannelCreate(uint64_t hash);
void lovrChannelDestroy(void* ref);
bool lovrChannelPush(Channel* channel, struct Variant* variant, double timeout, uint64_t* id);
bool lovrChannelPop(Channel* channel, struct Variant* variant, double timeout);
bool lovrChannelPeek(Channel* channel, struct Variant* variant);
void lovrChannelClear(Channel* channel);
uint64_t lovrChannelGetCount(Channel* channel);
bool lovrChannelHasRead(Channel* channel, uint64_t id);

View File

@ -67,7 +67,7 @@ typedef void* arr_allocator(void* data, size_t size);
#define arr_free(a) if ((a)->data) (a)->alloc((a)->data, 0)
#define arr_reserve(a, n) _arr_reserve((void**) &((a)->data), n, &(a)->capacity, sizeof(*(a)->data), (a)->alloc)
#define arr_expand(a, n) arr_reserve(a, (a)->length + n)
#define arr_push(a, x) arr_reserve(a, (a)->length + 1), (a)->data[(a)->length++] = x
#define arr_push(a, x) arr_reserve(a, (a)->length + 1), (a)->data[(a)->length] = x, (a)->length++
#define arr_pop(a) (a)->data[--(a)->length]
#define arr_append(a, p, n) arr_reserve(a, (a)->length + n), memcpy((a)->data + (a)->length, p, n * sizeof(*(p))), (a)->length += n
#define arr_splice(a, i, n) memmove((a)->data + (i), (a)->data + ((i) + n), ((a)->length - (i) - (n)) * sizeof(*(a)->data)), (a)->length -= n