mirror of
https://github.com/bjornbytes/lovr.git
synced 2024-07-03 21:13:42 +00:00
Start Canvas objects;
This commit is contained in:
parent
8eec247adb
commit
d261e4b00d
|
@ -1,6 +1,7 @@
|
||||||
#include "api/lovr.h"
|
#include "api/lovr.h"
|
||||||
#include "graphics/graphics.h"
|
#include "graphics/graphics.h"
|
||||||
#include "graphics/animator.h"
|
#include "graphics/animator.h"
|
||||||
|
#include "graphics/canvas.h"
|
||||||
#include "graphics/material.h"
|
#include "graphics/material.h"
|
||||||
#include "graphics/mesh.h"
|
#include "graphics/mesh.h"
|
||||||
#include "graphics/model.h"
|
#include "graphics/model.h"
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
map_int_t ArcModes;
|
map_int_t ArcModes;
|
||||||
map_int_t BlendAlphaModes;
|
map_int_t BlendAlphaModes;
|
||||||
map_int_t BlendModes;
|
map_int_t BlendModes;
|
||||||
|
map_int_t CanvasTypes;
|
||||||
map_int_t CompareModes;
|
map_int_t CompareModes;
|
||||||
map_int_t DrawModes;
|
map_int_t DrawModes;
|
||||||
map_int_t FilterModes;
|
map_int_t FilterModes;
|
||||||
|
@ -25,7 +27,6 @@ map_int_t MatrixTypes;
|
||||||
map_int_t MeshAttributeTypes;
|
map_int_t MeshAttributeTypes;
|
||||||
map_int_t MeshDrawModes;
|
map_int_t MeshDrawModes;
|
||||||
map_int_t MeshUsages;
|
map_int_t MeshUsages;
|
||||||
map_int_t TextureProjections;
|
|
||||||
map_int_t VerticalAligns;
|
map_int_t VerticalAligns;
|
||||||
map_int_t Windings;
|
map_int_t Windings;
|
||||||
map_int_t WrapModes;
|
map_int_t WrapModes;
|
||||||
|
@ -82,6 +83,7 @@ int l_lovrGraphicsInit(lua_State* L) {
|
||||||
luax_registertype(L, "Model", lovrModel);
|
luax_registertype(L, "Model", lovrModel);
|
||||||
luax_registertype(L, "Shader", lovrShader);
|
luax_registertype(L, "Shader", lovrShader);
|
||||||
luax_registertype(L, "Texture", lovrTexture);
|
luax_registertype(L, "Texture", lovrTexture);
|
||||||
|
luax_extendtype(L, "Texture", "Canvas", lovrTexture, lovrCanvas);
|
||||||
|
|
||||||
map_init(&ArcModes);
|
map_init(&ArcModes);
|
||||||
map_set(&ArcModes, "pie", ARC_MODE_PIE);
|
map_set(&ArcModes, "pie", ARC_MODE_PIE);
|
||||||
|
@ -102,6 +104,10 @@ int l_lovrGraphicsInit(lua_State* L) {
|
||||||
map_set(&BlendModes, "screen", BLEND_SCREEN);
|
map_set(&BlendModes, "screen", BLEND_SCREEN);
|
||||||
map_set(&BlendModes, "replace", BLEND_REPLACE);
|
map_set(&BlendModes, "replace", BLEND_REPLACE);
|
||||||
|
|
||||||
|
map_init(&CanvasTypes);
|
||||||
|
map_set(&CanvasTypes, "3d", CANVAS_3D);
|
||||||
|
map_set(&CanvasTypes, "2d", CANVAS_2D);
|
||||||
|
|
||||||
map_init(&CompareModes);
|
map_init(&CompareModes);
|
||||||
map_set(&CompareModes, "equal", COMPARE_EQUAL);
|
map_set(&CompareModes, "equal", COMPARE_EQUAL);
|
||||||
map_set(&CompareModes, "notequal", COMPARE_NOT_EQUAL);
|
map_set(&CompareModes, "notequal", COMPARE_NOT_EQUAL);
|
||||||
|
@ -154,10 +160,6 @@ int l_lovrGraphicsInit(lua_State* L) {
|
||||||
map_set(&MeshUsages, "dynamic", MESH_DYNAMIC);
|
map_set(&MeshUsages, "dynamic", MESH_DYNAMIC);
|
||||||
map_set(&MeshUsages, "stream", MESH_STREAM);
|
map_set(&MeshUsages, "stream", MESH_STREAM);
|
||||||
|
|
||||||
map_init(&TextureProjections);
|
|
||||||
map_set(&TextureProjections, "2d", PROJECTION_ORTHOGRAPHIC);
|
|
||||||
map_set(&TextureProjections, "3d", PROJECTION_PERSPECTIVE);
|
|
||||||
|
|
||||||
map_init(&VerticalAligns);
|
map_init(&VerticalAligns);
|
||||||
map_set(&VerticalAligns, "top", ALIGN_TOP);
|
map_set(&VerticalAligns, "top", ALIGN_TOP);
|
||||||
map_set(&VerticalAligns, "bottom", ALIGN_BOTTOM);
|
map_set(&VerticalAligns, "bottom", ALIGN_BOTTOM);
|
||||||
|
@ -696,6 +698,20 @@ int l_lovrGraphicsNewAnimator(lua_State* L) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int l_lovrGraphicsNewCanvas(lua_State* L) {
|
||||||
|
CanvasType type = CANVAS_3D;
|
||||||
|
int index = 1;
|
||||||
|
if (lua_type(L, index) == LUA_TSTRING) {
|
||||||
|
type = *(CanvasType*) luax_checkenum(L, index++, &CanvasTypes, "canvas type");
|
||||||
|
}
|
||||||
|
int width = luaL_checkinteger(L, index++);
|
||||||
|
int height = luaL_checkinteger(L, index++);
|
||||||
|
int msaa = luaL_optinteger(L, index++, 0);
|
||||||
|
Canvas* canvas = lovrCanvasCreate(type, width, height, msaa);
|
||||||
|
luax_pushtype(L, Canvas, canvas);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
int l_lovrGraphicsNewFont(lua_State* L) {
|
int l_lovrGraphicsNewFont(lua_State* L) {
|
||||||
Blob* blob = NULL;
|
Blob* blob = NULL;
|
||||||
float size;
|
float size;
|
||||||
|
@ -877,53 +893,42 @@ int l_lovrGraphicsNewShader(lua_State* L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrGraphicsNewTexture(lua_State* L) {
|
int l_lovrGraphicsNewTexture(lua_State* L) {
|
||||||
Texture* texture;
|
Blob* blobs[6];
|
||||||
|
bool isTable = lua_istable(L, 1);
|
||||||
|
int count = isTable ? lua_objlen(L, 1) : lua_gettop(L);
|
||||||
|
|
||||||
if (lua_type(L, 1) == LUA_TNUMBER) {
|
if (count != 1 && count != 6) {
|
||||||
int width = luaL_checknumber(L, 1);
|
return luaL_error(L, "Expected 1 image for a 2D texture or 6 images for a cube texture, got %d", count);
|
||||||
int height = luaL_checknumber(L, 2);
|
}
|
||||||
TextureProjection* projection = luax_optenum(L, 3, "3d", &TextureProjections, "projection");
|
|
||||||
int msaa = luaL_optnumber(L, 4, 0);
|
|
||||||
TextureData* textureData = lovrTextureDataGetEmpty(width, height, FORMAT_RGBA);
|
|
||||||
texture = lovrTextureCreateWithFramebuffer(textureData, *projection, msaa);
|
|
||||||
} else {
|
|
||||||
Blob* blobs[6];
|
|
||||||
bool isTable = lua_istable(L, 1);
|
|
||||||
int count = isTable ? lua_objlen(L, 1) : lua_gettop(L);
|
|
||||||
|
|
||||||
if (count != 1 && count != 6) {
|
if (isTable) {
|
||||||
return luaL_error(L, "Expected 1 image for a 2D texture or 6 images for a cube texture, got %d", count);
|
for (int i = 0; i < count; i++) {
|
||||||
}
|
lua_rawgeti(L, -1, i + 1);
|
||||||
|
blobs[i] = luax_readblob(L, -1, "Texture");
|
||||||
if (isTable) {
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
lua_rawgeti(L, -1, i + 1);
|
|
||||||
blobs[i] = luax_readblob(L, -1, "Texture");
|
|
||||||
lua_pop(L, 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
blobs[i] = luax_readblob(L, i + 1, "Texture");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool srgb = true;
|
|
||||||
if (lua_istable(L, count + 1)) {
|
|
||||||
lua_getfield(L, count + 1, "linear");
|
|
||||||
srgb = !lua_toboolean(L, -1);
|
|
||||||
lua_pop(L, 1);
|
lua_pop(L, 1);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
TextureData* slices[6];
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
slices[i] = lovrTextureDataFromBlob(blobs[i]);
|
blobs[i] = luax_readblob(L, i + 1, "Texture");
|
||||||
lovrRelease(&blobs[i]->ref);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextureType type = (count == 1) ? TEXTURE_2D : TEXTURE_CUBE;
|
|
||||||
texture = lovrTextureCreate(type, slices, count, srgb);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool srgb = true;
|
||||||
|
if (lua_istable(L, count + 1)) {
|
||||||
|
lua_getfield(L, count + 1, "linear");
|
||||||
|
srgb = !lua_toboolean(L, -1);
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureData* slices[6];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
slices[i] = lovrTextureDataFromBlob(blobs[i]);
|
||||||
|
lovrRelease(&blobs[i]->ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureType type = (count == 1) ? TEXTURE_2D : TEXTURE_CUBE;
|
||||||
|
Texture* texture = lovrTextureCreate(type, slices, count, srgb);
|
||||||
|
|
||||||
luax_pushtype(L, Texture, texture);
|
luax_pushtype(L, Texture, texture);
|
||||||
lovrRelease(&texture->ref);
|
lovrRelease(&texture->ref);
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -984,6 +989,7 @@ const luaL_Reg lovrGraphics[] = {
|
||||||
{ "skybox", l_lovrGraphicsSkybox },
|
{ "skybox", l_lovrGraphicsSkybox },
|
||||||
{ "print", l_lovrGraphicsPrint },
|
{ "print", l_lovrGraphicsPrint },
|
||||||
{ "newAnimator", l_lovrGraphicsNewAnimator },
|
{ "newAnimator", l_lovrGraphicsNewAnimator },
|
||||||
|
{ "newCanvas", l_lovrGraphicsNewCanvas },
|
||||||
{ "newFont", l_lovrGraphicsNewFont },
|
{ "newFont", l_lovrGraphicsNewFont },
|
||||||
{ "newMaterial", l_lovrGraphicsNewMaterial },
|
{ "newMaterial", l_lovrGraphicsNewMaterial },
|
||||||
{ "newMesh", l_lovrGraphicsNewMesh },
|
{ "newMesh", l_lovrGraphicsNewMesh },
|
||||||
|
|
|
@ -19,6 +19,7 @@ extern const luaL_Reg lovrAnimator[];
|
||||||
extern const luaL_Reg lovrAudio[];
|
extern const luaL_Reg lovrAudio[];
|
||||||
extern const luaL_Reg lovrBallJoint[];
|
extern const luaL_Reg lovrBallJoint[];
|
||||||
extern const luaL_Reg lovrBoxShape[];
|
extern const luaL_Reg lovrBoxShape[];
|
||||||
|
extern const luaL_Reg lovrCanvas[];
|
||||||
extern const luaL_Reg lovrCapsuleShape[];
|
extern const luaL_Reg lovrCapsuleShape[];
|
||||||
extern const luaL_Reg lovrController[];
|
extern const luaL_Reg lovrController[];
|
||||||
extern const luaL_Reg lovrCylinderShape[];
|
extern const luaL_Reg lovrCylinderShape[];
|
||||||
|
@ -50,6 +51,7 @@ extern const luaL_Reg lovrWorld[];
|
||||||
|
|
||||||
extern map_int_t BlendAlphaModes;
|
extern map_int_t BlendAlphaModes;
|
||||||
extern map_int_t BlendModes;
|
extern map_int_t BlendModes;
|
||||||
|
extern map_int_t CanvasTypes;
|
||||||
extern map_int_t CompareModes;
|
extern map_int_t CompareModes;
|
||||||
extern map_int_t ControllerAxes;
|
extern map_int_t ControllerAxes;
|
||||||
extern map_int_t ControllerButtons;
|
extern map_int_t ControllerButtons;
|
||||||
|
@ -70,7 +72,6 @@ extern map_int_t MeshDrawModes;
|
||||||
extern map_int_t MeshUsages;
|
extern map_int_t MeshUsages;
|
||||||
extern map_int_t PolygonWindings;
|
extern map_int_t PolygonWindings;
|
||||||
extern map_int_t ShapeTypes;
|
extern map_int_t ShapeTypes;
|
||||||
extern map_int_t TextureProjections;
|
|
||||||
extern map_int_t TimeUnits;
|
extern map_int_t TimeUnits;
|
||||||
extern map_int_t VerticalAligns;
|
extern map_int_t VerticalAligns;
|
||||||
extern map_int_t WrapModes;
|
extern map_int_t WrapModes;
|
||||||
|
|
19
src/api/types/canvas.c
Normal file
19
src/api/types/canvas.c
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#include "api/lovr.h"
|
||||||
|
#include "graphics/graphics.h"
|
||||||
|
#include "graphics/canvas.h"
|
||||||
|
|
||||||
|
int l_lovrCanvasRenderTo(lua_State* L) {
|
||||||
|
Canvas* canvas = luax_checktype(L, 1, Canvas);
|
||||||
|
lovrGraphicsPushView();
|
||||||
|
lovrCanvasBind(canvas);
|
||||||
|
lua_settop(L, 2);
|
||||||
|
lua_call(L, 0, 0);
|
||||||
|
lovrCanvasResolveMSAA(canvas);
|
||||||
|
lovrGraphicsPopView();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const luaL_Reg lovrCanvas[] = {
|
||||||
|
{ "renderto", l_lovrCanvasRenderTo },
|
||||||
|
{ NULL, NULL }
|
||||||
|
};
|
|
@ -2,14 +2,14 @@
|
||||||
#include "graphics/graphics.h"
|
#include "graphics/graphics.h"
|
||||||
|
|
||||||
int l_lovrTextureGetDimensions(lua_State* L) {
|
int l_lovrTextureGetDimensions(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
lua_pushnumber(L, texture->width);
|
lua_pushnumber(L, texture->width);
|
||||||
lua_pushnumber(L, texture->height);
|
lua_pushnumber(L, texture->height);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrTextureGetFilter(lua_State* L) {
|
int l_lovrTextureGetFilter(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
TextureFilter filter = lovrTextureGetFilter(texture);
|
TextureFilter filter = lovrTextureGetFilter(texture);
|
||||||
luax_pushenum(L, &FilterModes, filter.mode);
|
luax_pushenum(L, &FilterModes, filter.mode);
|
||||||
if (filter.mode == FILTER_ANISOTROPIC) {
|
if (filter.mode == FILTER_ANISOTROPIC) {
|
||||||
|
@ -20,19 +20,19 @@ int l_lovrTextureGetFilter(lua_State* L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrTextureGetHeight(lua_State* L) {
|
int l_lovrTextureGetHeight(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
lua_pushnumber(L, texture->height);
|
lua_pushnumber(L, texture->height);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrTextureGetWidth(lua_State* L) {
|
int l_lovrTextureGetWidth(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
lua_pushnumber(L, texture->width);
|
lua_pushnumber(L, texture->width);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrTextureGetWrap(lua_State* L) {
|
int l_lovrTextureGetWrap(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
TextureWrap wrap = lovrTextureGetWrap(texture);
|
TextureWrap wrap = lovrTextureGetWrap(texture);
|
||||||
luax_pushenum(L, &WrapModes, wrap.s);
|
luax_pushenum(L, &WrapModes, wrap.s);
|
||||||
luax_pushenum(L, &WrapModes, wrap.t);
|
luax_pushenum(L, &WrapModes, wrap.t);
|
||||||
|
@ -43,19 +43,8 @@ int l_lovrTextureGetWrap(lua_State* L) {
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrTextureRenderTo(lua_State* L) {
|
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
|
||||||
lovrGraphicsPushView();
|
|
||||||
lovrTextureBindFramebuffer(texture);
|
|
||||||
lua_settop(L, 2);
|
|
||||||
lua_call(L, 0, 0);
|
|
||||||
lovrTextureResolveMSAA(texture);
|
|
||||||
lovrGraphicsPopView();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int l_lovrTextureSetFilter(lua_State* L) {
|
int l_lovrTextureSetFilter(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
FilterMode mode = *(FilterMode*) luax_checkenum(L, 2, &FilterModes, "filter mode");
|
FilterMode mode = *(FilterMode*) luax_checkenum(L, 2, &FilterModes, "filter mode");
|
||||||
float anisotropy = luaL_optnumber(L, 3, 1.);
|
float anisotropy = luaL_optnumber(L, 3, 1.);
|
||||||
TextureFilter filter = { .mode = mode, .anisotropy = anisotropy };
|
TextureFilter filter = { .mode = mode, .anisotropy = anisotropy };
|
||||||
|
@ -64,7 +53,7 @@ int l_lovrTextureSetFilter(lua_State* L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int l_lovrTextureSetWrap(lua_State* L) {
|
int l_lovrTextureSetWrap(lua_State* L) {
|
||||||
Texture* texture = luax_checktype(L, 1, Texture);
|
Texture* texture = luax_checktypeof(L, 1, Texture);
|
||||||
TextureWrap wrap;
|
TextureWrap wrap;
|
||||||
wrap.s = *(WrapMode*) luax_checkenum(L, 2, &WrapModes, "wrap mode");
|
wrap.s = *(WrapMode*) luax_checkenum(L, 2, &WrapModes, "wrap mode");
|
||||||
wrap.t = *(WrapMode*) luax_optenum(L, 3, luaL_checkstring(L, 2), &WrapModes, "wrap mode");
|
wrap.t = *(WrapMode*) luax_optenum(L, 3, luaL_checkstring(L, 2), &WrapModes, "wrap mode");
|
||||||
|
@ -79,7 +68,6 @@ const luaL_Reg lovrTexture[] = {
|
||||||
{ "getHeight", l_lovrTextureGetHeight },
|
{ "getHeight", l_lovrTextureGetHeight },
|
||||||
{ "getWidth", l_lovrTextureGetWidth },
|
{ "getWidth", l_lovrTextureGetWidth },
|
||||||
{ "getWrap", l_lovrTextureGetWrap },
|
{ "getWrap", l_lovrTextureGetWrap },
|
||||||
{ "renderTo", l_lovrTextureRenderTo },
|
|
||||||
{ "setFilter", l_lovrTextureSetFilter },
|
{ "setFilter", l_lovrTextureSetFilter },
|
||||||
{ "setWrap", l_lovrTextureSetWrap },
|
{ "setWrap", l_lovrTextureSetWrap },
|
||||||
{ NULL, NULL }
|
{ NULL, NULL }
|
||||||
|
|
118
src/graphics/canvas.c
Normal file
118
src/graphics/canvas.c
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
#include "graphics/canvas.h"
|
||||||
|
#include "graphics/graphics.h"
|
||||||
|
#include "math/mat4.h"
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
Canvas* lovrCanvasCreate(CanvasType type, int width, int height, int msaa) {
|
||||||
|
TextureData* textureData = lovrTextureDataGetEmpty(width, height, FORMAT_RGBA);
|
||||||
|
Texture* texture = lovrTextureCreate(TEXTURE_2D, &textureData, 1, true);
|
||||||
|
if (!texture) return NULL;
|
||||||
|
|
||||||
|
Canvas* canvas = lovrAlloc(sizeof(Canvas), lovrCanvasDestroy);
|
||||||
|
canvas->texture = *texture;
|
||||||
|
canvas->type = type;
|
||||||
|
canvas->msaa = msaa;
|
||||||
|
canvas->framebuffer = 0;
|
||||||
|
canvas->resolveFramebuffer = 0;
|
||||||
|
canvas->depthBuffer = 0;
|
||||||
|
canvas->msaaTexture = 0;
|
||||||
|
|
||||||
|
// Framebuffer
|
||||||
|
glGenFramebuffers(1, &canvas->framebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, canvas->framebuffer);
|
||||||
|
|
||||||
|
// Color attachment
|
||||||
|
if (msaa > 0) {
|
||||||
|
GLenum format = lovrGraphicsIsGammaCorrect() ? GL_SRGB8_ALPHA8 : GL_RGBA8;
|
||||||
|
glGenRenderbuffers(1, &canvas->msaaTexture);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, canvas->msaaTexture);
|
||||||
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, format, width, height);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, canvas->msaaTexture);
|
||||||
|
} else {
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, canvas->texture.id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depth attachment
|
||||||
|
if (type == CANVAS_3D) {
|
||||||
|
glGenRenderbuffers(1, &canvas->depthBuffer);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, canvas->depthBuffer);
|
||||||
|
if (msaa > 0) {
|
||||||
|
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH_COMPONENT, width, height);
|
||||||
|
} else {
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
|
||||||
|
}
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, canvas->depthBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve framebuffer
|
||||||
|
if (msaa > 0) {
|
||||||
|
glGenFramebuffers(1, &canvas->resolveFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, canvas->resolveFramebuffer);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, canvas->texture.id);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, canvas->texture.id, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, canvas->framebuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
lovrAssert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Error creating texture");
|
||||||
|
lovrGraphicsClear(true, true);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lovrCanvasDestroy(const Ref* ref) {
|
||||||
|
Canvas* canvas = (Canvas*) containerof(ref, Texture);
|
||||||
|
glDeleteFramebuffers(1, &canvas->framebuffer);
|
||||||
|
if (canvas->resolveFramebuffer) {
|
||||||
|
glDeleteFramebuffers(1, &canvas->resolveFramebuffer);
|
||||||
|
}
|
||||||
|
if (canvas->depthBuffer) {
|
||||||
|
glDeleteRenderbuffers(1, &canvas->depthBuffer);
|
||||||
|
}
|
||||||
|
if (canvas->msaaTexture) {
|
||||||
|
glDeleteTextures(1, &canvas->msaaTexture);
|
||||||
|
}
|
||||||
|
lovrTextureDestroy(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lovrCanvasBind(Canvas* canvas) {
|
||||||
|
int width = canvas->texture.width;
|
||||||
|
int height = canvas->texture.height;
|
||||||
|
lovrGraphicsBindFramebuffer(canvas->framebuffer);
|
||||||
|
lovrGraphicsSetViewport(0, 0, width, height);
|
||||||
|
|
||||||
|
if (canvas->type == CANVAS_2D) {
|
||||||
|
float projection[16];
|
||||||
|
mat4_orthographic(projection, 0, width, 0, height, -1, 1);
|
||||||
|
lovrGraphicsSetProjection(projection);
|
||||||
|
} else {
|
||||||
|
mat4 projection = lovrGraphicsGetProjection();
|
||||||
|
float b = projection[5];
|
||||||
|
float c = projection[10];
|
||||||
|
float d = projection[14];
|
||||||
|
float aspect = (float) width / height;
|
||||||
|
float k = (c - 1.f) / (c + 1.f);
|
||||||
|
float near = (d * (1.f - k)) / (2.f * k);
|
||||||
|
float far = k * near;
|
||||||
|
float fov = -2.f * atan(1.f / b);
|
||||||
|
float newProjection[16];
|
||||||
|
mat4_perspective(newProjection, near, far, fov, aspect);
|
||||||
|
lovrGraphicsSetProjection(newProjection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void lovrCanvasResolveMSAA(Canvas* canvas) {
|
||||||
|
if (canvas->msaa == 0) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = canvas->texture.width;
|
||||||
|
int height = canvas->texture.height;
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, canvas->framebuffer);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, canvas->resolveFramebuffer);
|
||||||
|
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
|
}
|
22
src/graphics/canvas.h
Normal file
22
src/graphics/canvas.h
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
#include "graphics/texture.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CANVAS_3D,
|
||||||
|
CANVAS_2D
|
||||||
|
} CanvasType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Texture texture;
|
||||||
|
CanvasType type;
|
||||||
|
GLuint framebuffer;
|
||||||
|
GLuint resolveFramebuffer;
|
||||||
|
GLuint depthBuffer;
|
||||||
|
GLuint msaaTexture;
|
||||||
|
int msaa;
|
||||||
|
} Canvas;
|
||||||
|
|
||||||
|
Canvas* lovrCanvasCreate(CanvasType, int width, int height, int msaa);
|
||||||
|
void lovrCanvasDestroy(const Ref* ref);
|
||||||
|
void lovrCanvasBind(Canvas* canvas);
|
||||||
|
void lovrCanvasResolveMSAA(Canvas* canvas);
|
|
@ -1,7 +1,5 @@
|
||||||
#include "graphics/texture.h"
|
#include "graphics/texture.h"
|
||||||
#include "graphics/graphics.h"
|
#include "graphics/graphics.h"
|
||||||
#include "math/mat4.h"
|
|
||||||
#include <math.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
|
@ -96,8 +94,6 @@ Texture* lovrTextureCreate(TextureType type, TextureData* slices[6], int sliceCo
|
||||||
validateSlices(type, slices, sliceCount);
|
validateSlices(type, slices, sliceCount);
|
||||||
texture->sliceCount = sliceCount;
|
texture->sliceCount = sliceCount;
|
||||||
memcpy(texture->slices, slices, sliceCount * sizeof(TextureData*));
|
memcpy(texture->slices, slices, sliceCount * sizeof(TextureData*));
|
||||||
texture->framebuffer = 0;
|
|
||||||
texture->depthBuffer = 0;
|
|
||||||
texture->srgb = srgb;
|
texture->srgb = srgb;
|
||||||
glGenTextures(1, &texture->id);
|
glGenTextures(1, &texture->id);
|
||||||
lovrGraphicsBindTexture(texture, type, 0);
|
lovrGraphicsBindTexture(texture, type, 0);
|
||||||
|
@ -109,109 +105,15 @@ Texture* lovrTextureCreate(TextureType type, TextureData* slices[6], int sliceCo
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
Texture* lovrTextureCreateWithFramebuffer(TextureData* textureData, TextureProjection projection, int msaa) {
|
|
||||||
Texture* texture = lovrTextureCreate(TEXTURE_2D, &textureData, 1, true);
|
|
||||||
if (!texture) return NULL;
|
|
||||||
|
|
||||||
int width = texture->width;
|
|
||||||
int height = texture->height;
|
|
||||||
texture->projection = projection;
|
|
||||||
texture->msaa = msaa;
|
|
||||||
|
|
||||||
// Framebuffer
|
|
||||||
glGenFramebuffers(1, &texture->framebuffer);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, texture->framebuffer);
|
|
||||||
|
|
||||||
// Color attachment
|
|
||||||
if (msaa) {
|
|
||||||
GLenum format = lovrGraphicsIsGammaCorrect() ? GL_SRGB8_ALPHA8 : GL_RGBA8;
|
|
||||||
glGenRenderbuffers(1, &texture->msaaId);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, texture->msaaId);
|
|
||||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, format, width, height);
|
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, texture->msaaId);
|
|
||||||
} else {
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture->id, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Depth attachment
|
|
||||||
if (projection == PROJECTION_PERSPECTIVE) {
|
|
||||||
glGenRenderbuffers(1, &texture->depthBuffer);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, texture->depthBuffer);
|
|
||||||
if (msaa) {
|
|
||||||
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa, GL_DEPTH_COMPONENT, width, height);
|
|
||||||
} else {
|
|
||||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
|
|
||||||
}
|
|
||||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, texture->depthBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve framebuffer
|
|
||||||
if (msaa) {
|
|
||||||
glGenFramebuffers(1, &texture->resolveFramebuffer);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, texture->resolveFramebuffer);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture->id);
|
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture->id, 0);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, texture->framebuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
lovrAssert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "Error creating texture");
|
|
||||||
lovrGraphicsClear(true, true);
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
void lovrTextureDestroy(const Ref* ref) {
|
void lovrTextureDestroy(const Ref* ref) {
|
||||||
Texture* texture = containerof(ref, Texture);
|
Texture* texture = containerof(ref, Texture);
|
||||||
for (int i = 0; i < texture->sliceCount; i++) {
|
for (int i = 0; i < texture->sliceCount; i++) {
|
||||||
lovrTextureDataDestroy(texture->slices[i]);
|
lovrTextureDataDestroy(texture->slices[i]);
|
||||||
}
|
}
|
||||||
if (texture->framebuffer) {
|
|
||||||
glDeleteFramebuffers(1, &texture->framebuffer);
|
|
||||||
}
|
|
||||||
glDeleteTextures(1, &texture->id);
|
glDeleteTextures(1, &texture->id);
|
||||||
free(texture);
|
free(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
void lovrTextureBindFramebuffer(Texture* texture) {
|
|
||||||
lovrAssert(texture->framebuffer, "Texture cannot be used as a canvas");
|
|
||||||
lovrGraphicsBindFramebuffer(texture->framebuffer);
|
|
||||||
lovrGraphicsSetViewport(0, 0, texture->width, texture->height);
|
|
||||||
|
|
||||||
if (texture->projection == PROJECTION_ORTHOGRAPHIC) {
|
|
||||||
float projection[16];
|
|
||||||
mat4_orthographic(projection, 0, texture->width, 0, texture->height, -1, 1);
|
|
||||||
lovrGraphicsSetProjection(projection);
|
|
||||||
} else if (texture->projection == PROJECTION_PERSPECTIVE) {
|
|
||||||
mat4 projection = lovrGraphicsGetProjection();
|
|
||||||
float b = projection[5];
|
|
||||||
float c = projection[10];
|
|
||||||
float d = projection[14];
|
|
||||||
float aspect = (float) texture->width / texture->height;
|
|
||||||
float k = (c - 1.f) / (c + 1.f);
|
|
||||||
float near = (d * (1.f - k)) / (2.f * k);
|
|
||||||
float far = k * near;
|
|
||||||
float fov = -2.f * atan(1.f / b);
|
|
||||||
float newProjection[16];
|
|
||||||
mat4_perspective(newProjection, near, far, fov, aspect);
|
|
||||||
lovrGraphicsSetProjection(newProjection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void lovrTextureResolveMSAA(Texture* texture) {
|
|
||||||
if (!texture->msaa) {
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = texture->width;
|
|
||||||
int height = texture->height;
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, texture->framebuffer);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, texture->resolveFramebuffer);
|
|
||||||
glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
|
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextureFilter lovrTextureGetFilter(Texture* texture) {
|
TextureFilter lovrTextureGetFilter(Texture* texture) {
|
||||||
return texture->filter;
|
return texture->filter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,11 +33,6 @@ typedef struct {
|
||||||
WrapMode r;
|
WrapMode r;
|
||||||
} TextureWrap;
|
} TextureWrap;
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
PROJECTION_ORTHOGRAPHIC,
|
|
||||||
PROJECTION_PERSPECTIVE
|
|
||||||
} TextureProjection;
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
Ref ref;
|
Ref ref;
|
||||||
TextureType type;
|
TextureType type;
|
||||||
|
@ -46,24 +41,15 @@ typedef struct {
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
GLuint id;
|
GLuint id;
|
||||||
GLuint msaaId;
|
|
||||||
GLuint framebuffer;
|
|
||||||
GLuint resolveFramebuffer;
|
|
||||||
GLuint depthBuffer;
|
|
||||||
TextureProjection projection;
|
|
||||||
TextureFilter filter;
|
TextureFilter filter;
|
||||||
TextureWrap wrap;
|
TextureWrap wrap;
|
||||||
int msaa;
|
|
||||||
bool srgb;
|
bool srgb;
|
||||||
} Texture;
|
} Texture;
|
||||||
|
|
||||||
GLenum lovrTextureGetGLFormat(TextureFormat format);
|
GLenum lovrTextureGetGLFormat(TextureFormat format);
|
||||||
|
|
||||||
Texture* lovrTextureCreate(TextureType type, TextureData* data[6], int count, bool srgb);
|
Texture* lovrTextureCreate(TextureType type, TextureData* data[6], int count, bool srgb);
|
||||||
Texture* lovrTextureCreateWithFramebuffer(TextureData* textureData, TextureProjection projection, int msaa);
|
|
||||||
void lovrTextureDestroy(const Ref* ref);
|
void lovrTextureDestroy(const Ref* ref);
|
||||||
void lovrTextureBindFramebuffer(Texture* texture);
|
|
||||||
void lovrTextureResolveMSAA(Texture* texture);
|
|
||||||
TextureFilter lovrTextureGetFilter(Texture* texture);
|
TextureFilter lovrTextureGetFilter(Texture* texture);
|
||||||
void lovrTextureSetFilter(Texture* texture, TextureFilter filter);
|
void lovrTextureSetFilter(Texture* texture, TextureFilter filter);
|
||||||
TextureWrap lovrTextureGetWrap(Texture* texture);
|
TextureWrap lovrTextureGetWrap(Texture* texture);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
#include "math/mat4.h"
|
#include "math/mat4.h"
|
||||||
#include "math/quat.h"
|
#include "math/quat.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "graphics/texture.h"
|
#include "graphics/canvas.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
@ -51,12 +51,11 @@ typedef struct {
|
||||||
float refreshRate;
|
float refreshRate;
|
||||||
float vsyncToPhotons;
|
float vsyncToPhotons;
|
||||||
|
|
||||||
Texture* texture;
|
Canvas* canvas;
|
||||||
} HeadsetState;
|
} HeadsetState;
|
||||||
|
|
||||||
static HeadsetState state;
|
static HeadsetState state;
|
||||||
|
|
||||||
|
|
||||||
static bool openvrIsAvailable() {
|
static bool openvrIsAvailable() {
|
||||||
if (VR_IsHmdPresent() && VR_IsRuntimeInstalled()) {
|
if (VR_IsHmdPresent() && VR_IsRuntimeInstalled()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -147,7 +146,7 @@ static void openvrInit() {
|
||||||
state.isInitialized = false;
|
state.isInitialized = false;
|
||||||
state.isRendering = false;
|
state.isRendering = false;
|
||||||
state.isMirrored = true;
|
state.isMirrored = true;
|
||||||
state.texture = NULL;
|
state.canvas = NULL;
|
||||||
vec_init(&state.controllers);
|
vec_init(&state.controllers);
|
||||||
|
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
|
@ -221,8 +220,8 @@ static void openvrInit() {
|
||||||
|
|
||||||
static void openvrDestroy() {
|
static void openvrDestroy() {
|
||||||
state.isInitialized = false;
|
state.isInitialized = false;
|
||||||
if (state.texture) {
|
if (state.canvas) {
|
||||||
lovrRelease(&state.texture->ref);
|
lovrRelease(&state.canvas->texture.ref);
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
if (state.deviceModels[i]) {
|
if (state.deviceModels[i]) {
|
||||||
|
@ -737,10 +736,9 @@ static ModelData* openvrControllerNewModelData(Controller* controller) {
|
||||||
static void openvrRenderTo(headsetRenderCallback callback, void* userdata) {
|
static void openvrRenderTo(headsetRenderCallback callback, void* userdata) {
|
||||||
if (!state.isInitialized) return;
|
if (!state.isInitialized) return;
|
||||||
|
|
||||||
if (!state.texture) {
|
if (!state.canvas) {
|
||||||
state.system->GetRecommendedRenderTargetSize(&state.renderWidth, &state.renderHeight);
|
state.system->GetRecommendedRenderTargetSize(&state.renderWidth, &state.renderHeight);
|
||||||
TextureData* textureData = lovrTextureDataGetEmpty(state.renderWidth, state.renderHeight, FORMAT_RGBA);
|
state.canvas = lovrCanvasCreate(CANVAS_3D, state.renderWidth, state.renderHeight, 4);
|
||||||
state.texture = lovrTextureCreateWithFramebuffer(textureData, PROJECTION_PERSPECTIVE, 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float head[16], transform[16], projection[16];
|
float head[16], transform[16], projection[16];
|
||||||
|
@ -767,21 +765,21 @@ static void openvrRenderTo(headsetRenderCallback callback, void* userdata) {
|
||||||
mat4_fromMat44(projection, matrix);
|
mat4_fromMat44(projection, matrix);
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
lovrTextureBindFramebuffer(state.texture);
|
lovrCanvasBind(state.canvas);
|
||||||
lovrGraphicsPush();
|
lovrGraphicsPush();
|
||||||
lovrGraphicsMatrixTransform(MATRIX_VIEW, transform);
|
lovrGraphicsMatrixTransform(MATRIX_VIEW, transform);
|
||||||
lovrGraphicsSetProjection(projection);
|
lovrGraphicsSetProjection(projection);
|
||||||
lovrGraphicsClear(true, true);
|
lovrGraphicsClear(true, true);
|
||||||
callback(eye, userdata);
|
callback(eye, userdata);
|
||||||
lovrGraphicsPop();
|
lovrGraphicsPop();
|
||||||
lovrTextureResolveMSAA(state.texture);
|
lovrCanvasResolveMSAA(state.canvas);
|
||||||
|
|
||||||
// OpenVR changes the OpenGL texture binding, so we reset it after rendering
|
// OpenVR changes the OpenGL texture binding, so we reset it after rendering
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
Texture* oldTexture = lovrGraphicsGetTexture(0);
|
Texture* oldTexture = lovrGraphicsGetTexture(0);
|
||||||
|
|
||||||
// Submit
|
// Submit
|
||||||
uintptr_t texture = (uintptr_t) state.texture->id;
|
uintptr_t texture = (uintptr_t) state.canvas->texture.id;
|
||||||
ETextureType textureType = ETextureType_TextureType_OpenGL;
|
ETextureType textureType = ETextureType_TextureType_OpenGL;
|
||||||
EColorSpace colorSpace = lovrGraphicsIsGammaCorrect() ? EColorSpace_ColorSpace_Linear : EColorSpace_ColorSpace_Gamma;
|
EColorSpace colorSpace = lovrGraphicsIsGammaCorrect() ? EColorSpace_ColorSpace_Linear : EColorSpace_ColorSpace_Gamma;
|
||||||
Texture_t eyeTexture = { (void*) texture, textureType, colorSpace };
|
Texture_t eyeTexture = { (void*) texture, textureType, colorSpace };
|
||||||
|
@ -805,7 +803,7 @@ static void openvrRenderTo(headsetRenderCallback callback, void* userdata) {
|
||||||
}
|
}
|
||||||
|
|
||||||
lovrGraphicsSetShader(NULL);
|
lovrGraphicsSetShader(NULL);
|
||||||
lovrGraphicsPlaneFullscreen(state.texture);
|
lovrGraphicsPlaneFullscreen(&state.canvas->texture);
|
||||||
lovrGraphicsSetShader(lastShader);
|
lovrGraphicsSetShader(lastShader);
|
||||||
|
|
||||||
if (lastShader) {
|
if (lastShader) {
|
||||||
|
|
Loading…
Reference in a new issue