mirror of
https://github.com/XProger/OpenLara.git
synced 2025-03-18 18:19:39 +01:00
GCW Zero platform support (RG350)
This commit is contained in:
parent
4a25746bd4
commit
8c8f539a70
@ -310,10 +310,16 @@ struct Character : Controller {
|
||||
void bakeEnvironment(Texture *&environment) {
|
||||
flags.invisible = true;
|
||||
if (!environment) {
|
||||
environment = new Texture(256, 256, 1, FMT_RGBA, OPT_CUBEMAP | OPT_MIPMAPS | OPT_TARGET);
|
||||
uint32 opt = OPT_CUBEMAP | OPT_TARGET;
|
||||
#ifdef USE_CUBEMAP_MIPS
|
||||
opt |= OPT_MIPMAPS;
|
||||
#endif
|
||||
environment = new Texture(256, 256, 1, FMT_RGB16, opt);
|
||||
}
|
||||
game->renderEnvironment(getRoomIndex(), pos - vec3(0.0f, 384.0f, 0.0f), &environment);
|
||||
environment->generateMipMap();
|
||||
#ifdef USE_CUBEMAP_MIPS
|
||||
environment->generateMipMap();
|
||||
#endif
|
||||
flags.invisible = false;
|
||||
}
|
||||
};
|
||||
|
42
src/core.h
42
src/core.h
@ -12,6 +12,8 @@
|
||||
#define OS_FILEIO_CACHE
|
||||
#define OS_PTHREAD_MT
|
||||
|
||||
#define USE_CUBEMAP_MIPS
|
||||
|
||||
#ifdef WIN32
|
||||
#define _OS_WIN 1
|
||||
#define _GAPI_GL 1
|
||||
@ -64,6 +66,15 @@
|
||||
#define _OS_BITTBOY 1
|
||||
#define _OS_LINUX 1
|
||||
#define _GAPI_SW 1
|
||||
#elif __GCW0__
|
||||
#define _OS_GCW0 1
|
||||
#define _GAPI_GL 1
|
||||
#define _GAPI_GLES 1
|
||||
|
||||
#define DYNGEOM_NO_VBO
|
||||
|
||||
// etnaviv driver has a bug with cubemap mips generator
|
||||
#undef USE_CUBEMAP_MIPS
|
||||
#elif __linux__
|
||||
#define _OS_LINUX 1
|
||||
#define _GAPI_GL 1
|
||||
@ -129,7 +140,12 @@
|
||||
#define SPLIT_BY_CLUT
|
||||
#endif
|
||||
#else
|
||||
#define MERGE_MODELS
|
||||
// current etnaviv driver implementation uses uncompatible Mesa GLSL compiler
|
||||
// it produce unimplemented TRUNC/ARL instructions instead of F2I
|
||||
// so we can't use joints indexing in the shader (see MESH_SKINNING)
|
||||
#ifndef _OS_GCW0
|
||||
#define MERGE_MODELS
|
||||
#endif
|
||||
#define MERGE_SPRITES
|
||||
#define GENERATE_WATER_PLANE
|
||||
#endif
|
||||
@ -137,8 +153,10 @@
|
||||
#include "utils.h"
|
||||
|
||||
// muse be equal with base shader
|
||||
#ifdef __OS_3DS
|
||||
#if defined(_OS_3DS)
|
||||
#define SHADOW_TEX_SIZE 512
|
||||
#elif defined(_OS_GCW0)
|
||||
#define SHADOW_TEX_SIZE 256
|
||||
#else
|
||||
#define SHADOW_TEX_SIZE 2048
|
||||
#endif
|
||||
@ -235,6 +253,7 @@ namespace Core {
|
||||
bool tex3D;
|
||||
bool texRG;
|
||||
bool texBorder;
|
||||
bool texMaxLevel;
|
||||
bool colorFloat, texFloat, texFloatLinear;
|
||||
bool colorHalf, texHalf, texHalfLinear;
|
||||
#ifdef PROFILE
|
||||
@ -454,7 +473,11 @@ struct PSO {
|
||||
uint32 renderState;
|
||||
};
|
||||
|
||||
typedef uint32 Index;
|
||||
#if defined(_OS_WIN) || defined(_OS_LINUX) || defined(_OS_MAC) || defined(_OS_WEB)
|
||||
typedef uint32 Index;
|
||||
#else
|
||||
typedef uint16 Index;
|
||||
#endif
|
||||
|
||||
struct Edge {
|
||||
Index a, b;
|
||||
@ -738,8 +761,9 @@ namespace Core {
|
||||
lightStackCount = 0;
|
||||
|
||||
memset(&support, 0, sizeof(support));
|
||||
support.texMinSize = 1;
|
||||
Core::support.derivatives = true;
|
||||
support.texMinSize = 1;
|
||||
support.texMaxLevel = true;
|
||||
support.derivatives = true;
|
||||
|
||||
#ifdef USE_INFLATE
|
||||
tinf_init();
|
||||
@ -765,6 +789,7 @@ namespace Core {
|
||||
LOG(" 3D textures : %s\n", support.tex3D ? "true" : "false");
|
||||
LOG(" RG textures : %s\n", support.texRG ? "true" : "false");
|
||||
LOG(" border color : %s\n", support.texBorder ? "true" : "false");
|
||||
LOG(" max level : %s\n", support.texMaxLevel ? "true" : "false");
|
||||
LOG(" anisotropic : %d\n", support.maxAniso);
|
||||
LOG(" float textures : float = %s, half = %s\n",
|
||||
support.colorFloat ? "full" : (support.texFloat ? (support.texFloatLinear ? "linear" : "nearest") : "false"),
|
||||
@ -899,6 +924,13 @@ namespace Core {
|
||||
settings.detail.setLighting (Core::Settings::MEDIUM);
|
||||
#endif
|
||||
|
||||
#if defined(_OS_GCW0)
|
||||
settings.detail.setFilter (Core::Settings::MEDIUM);
|
||||
settings.detail.setShadows (Core::Settings::MEDIUM);
|
||||
settings.detail.setLighting (Core::Settings::MEDIUM);
|
||||
settings.audio.subtitles = false;
|
||||
#endif
|
||||
|
||||
#ifdef _OS_PSC
|
||||
settings.detail.setLighting (Core::Settings::MEDIUM);
|
||||
settings.detail.setShadows (Core::Settings::LOW);
|
||||
|
@ -58,7 +58,7 @@ namespace Game {
|
||||
level->init(playLogo, playVideo);
|
||||
|
||||
UI::game = level;
|
||||
#if !defined(_OS_PSP) && !defined(_OS_CLOVER)
|
||||
#if !defined(INV_GAMEPAD_ONLY)
|
||||
UI::helpTipTime = 5.0f;
|
||||
#endif
|
||||
delete lvl;
|
||||
|
118
src/gapi/gl.h
118
src/gapi/gl.h
@ -116,6 +116,50 @@
|
||||
#elif defined(_OS_PSC)
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
extern EGLDisplay display;
|
||||
#elif defined(_OS_GCW0)
|
||||
#include <GLES2/gl2.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#define GL_TEXTURE_MAX_LEVEL GL_TEXTURE_MAX_LEVEL_APPLE
|
||||
#define GL_TEXTURE_3D 0
|
||||
#define GL_TEXTURE_WRAP_R 0
|
||||
#define GL_TEXTURE_COMPARE_MODE 0x884C
|
||||
#define GL_TEXTURE_COMPARE_FUNC 0x884D
|
||||
#define GL_COMPARE_REF_TO_TEXTURE 0x884E
|
||||
|
||||
#undef GL_RG
|
||||
#undef GL_RG32F
|
||||
#undef GL_RG16F
|
||||
#undef GL_RGBA32F
|
||||
#undef GL_RGBA16F
|
||||
#undef GL_HALF_FLOAT
|
||||
|
||||
#define GL_RG GL_RGBA
|
||||
#define GL_RGBA32F GL_RGBA
|
||||
#define GL_RGBA16F GL_RGBA
|
||||
#define GL_RG32F GL_RGBA
|
||||
#define GL_RG16F GL_RGBA
|
||||
#define GL_HALF_FLOAT GL_HALF_FLOAT_OES
|
||||
|
||||
#define glTexImage3D(...) 0
|
||||
|
||||
#define GL_PROGRAM_BINARY_LENGTH GL_PROGRAM_BINARY_LENGTH_OES
|
||||
|
||||
#define PFNGLGENVERTEXARRAYSPROC PFNGLGENVERTEXARRAYSOESPROC
|
||||
#define PFNGLDELETEVERTEXARRAYSPROC PFNGLDELETEVERTEXARRAYSOESPROC
|
||||
#define PFNGLBINDVERTEXARRAYPROC PFNGLBINDVERTEXARRAYOESPROC
|
||||
#define PFNGLGETPROGRAMBINARYPROC PFNGLGETPROGRAMBINARYOESPROC
|
||||
#define PFNGLPROGRAMBINARYPROC PFNGLPROGRAMBINARYOESPROC
|
||||
|
||||
#define glGenVertexArrays glGenVertexArraysOES
|
||||
#define glDeleteVertexArrays glDeleteVertexArraysOES
|
||||
#define glBindVertexArray glBindVertexArrayOES
|
||||
#define glGetProgramBinary glGetProgramBinaryOES
|
||||
#define glProgramBinary glProgramBinaryOES
|
||||
|
||||
extern EGLDisplay display;
|
||||
#elif defined(_OS_RPI) || defined(_OS_CLOVER)
|
||||
#include <GLES2/gl2.h>
|
||||
@ -158,7 +202,7 @@
|
||||
#define GL_PROGRAM_BINARY_LENGTH GL_PROGRAM_BINARY_LENGTH_OES
|
||||
#define glGetProgramBinary(...)
|
||||
#define glProgramBinary(...)
|
||||
|
||||
|
||||
extern EGLDisplay display;
|
||||
#elif _OS_SWITCH
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
@ -248,21 +292,17 @@
|
||||
#define glProgramBinary(...)
|
||||
#endif
|
||||
|
||||
#if defined(_OS_WIN) || defined(_OS_LINUX)
|
||||
#if defined(_OS_WIN) || defined(_OS_LINUX) || defined(_OS_GCW0)
|
||||
|
||||
#ifdef _OS_ANDROID
|
||||
#define GetProc(x) dlsym(libGL, x);
|
||||
#else
|
||||
void* GetProc(const char *name) {
|
||||
#ifdef _OS_WIN
|
||||
return (void*)wglGetProcAddress(name);
|
||||
#elif _OS_LINUX
|
||||
return (void*)glXGetProcAddress((GLubyte*)name);
|
||||
#else // EGL
|
||||
return (void*)eglGetProcAddress(name);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
void* GetProc(const char *name) {
|
||||
#ifdef _OS_WIN
|
||||
return (void*)wglGetProcAddress(name);
|
||||
#elif _OS_LINUX
|
||||
return (void*)glXGetProcAddress((GLubyte*)name);
|
||||
#else // EGL
|
||||
return (void*)eglGetProcAddress(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
#define GetProcOGL(x) x=(decltype(x))GetProc(#x);
|
||||
|
||||
@ -341,15 +381,17 @@
|
||||
PFNGLBUFFERSUBDATAARBPROC glBufferSubData;
|
||||
#endif
|
||||
|
||||
// Vertex Arrays
|
||||
PFNGLGENVERTEXARRAYSPROC glGenVertexArrays;
|
||||
PFNGLDELETEVERTEXARRAYSPROC glDeleteVertexArrays;
|
||||
PFNGLBINDVERTEXARRAYPROC glBindVertexArray;
|
||||
// Binary shaders
|
||||
PFNGLGETPROGRAMBINARYPROC glGetProgramBinary;
|
||||
PFNGLPROGRAMBINARYPROC glProgramBinary;
|
||||
#endif
|
||||
|
||||
#if defined(_GAPI_GLES) && !defined(_OS_RPI) && !defined(_OS_CLOVER) && !defined(_OS_PSC) && !defined(_OS_IOS) && !defined(_OS_ANDROID) && !defined(__SDL2__)
|
||||
PFNGLDISCARDFRAMEBUFFEREXTPROC glDiscardFramebufferEXT;
|
||||
#if defined(_GAPI_GLES)
|
||||
PFNGLDISCARDFRAMEBUFFEREXTPROC glDiscardFramebufferEXT;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef PROFILE
|
||||
@ -560,7 +602,18 @@ namespace GAPI {
|
||||
sprintf(defines + strlen(defines), "#define %s\n", DefineName[def[i]]);
|
||||
}
|
||||
|
||||
#if defined(_OS_RPI) || defined(_OS_CLOVER) || (defined (__SDL2__) && defined (_GAPI_GLES))
|
||||
sprintf(defines + strlen(defines), "#define SHADOW_SIZE %d.0\n", SHADOW_TEX_SIZE);
|
||||
|
||||
#ifdef MERGE_MODELS
|
||||
strcat(defines, "#define MESH_SKINNING\n");
|
||||
#endif
|
||||
|
||||
// etnaviv driver has no sin/cos/abs implementation
|
||||
#if !defined(_OS_GCW0)
|
||||
strcat(defines, "#define VERT_CAUSTICS\n");
|
||||
#endif
|
||||
|
||||
#if defined(_OS_RPI) || defined(_OS_CLOVER) || defined(_OS_GCW0) || (defined (__SDL2__) && defined(_GAPI_GLES))
|
||||
strcat(defines, "#define OPT_VLIGHTPROJ\n");
|
||||
strcat(defines, "#define OPT_VLIGHTVEC\n");
|
||||
strcat(defines, "#define OPT_SHADOW_ONETAP\n");
|
||||
@ -858,9 +911,9 @@ namespace GAPI {
|
||||
glGenerateMipmap(target);
|
||||
if ((opt & (OPT_VOLUME | OPT_CUBEMAP | OPT_NEAREST)) == 0 && (Core::support.maxAniso > 0)) {
|
||||
glTexParameteri(target, GL_TEXTURE_MAX_ANISOTROPY_EXT, min(int(Core::support.maxAniso), 8));
|
||||
#if !defined(_OS_RPI) && !defined(_OS_CLOVER) && !(defined (__SDL2__) && defined (_GAPI_GLES))// TODO
|
||||
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 3);
|
||||
#endif
|
||||
if (Core::support.texMaxLevel) {
|
||||
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1062,7 +1115,7 @@ namespace GAPI {
|
||||
//void *libGL = dlopen("libGLESv2.so", RTLD_LAZY);
|
||||
#endif
|
||||
|
||||
#if defined(_OS_WIN) || defined(_OS_LINUX)
|
||||
#if defined(_OS_WIN) || defined(_OS_LINUX) || defined(_OS_GCW0)
|
||||
#ifdef _OS_WIN
|
||||
GetProcOGL(glActiveTexture);
|
||||
#endif
|
||||
@ -1135,10 +1188,11 @@ namespace GAPI {
|
||||
GetProcOGL(glGenVertexArrays);
|
||||
GetProcOGL(glDeleteVertexArrays);
|
||||
GetProcOGL(glBindVertexArray);
|
||||
|
||||
GetProcOGL(glGetProgramBinary);
|
||||
GetProcOGL(glProgramBinary);
|
||||
|
||||
#ifdef _GAPI_GLES
|
||||
#if defined(_GAPI_GLES)
|
||||
GetProcOGL(glDiscardFramebufferEXT);
|
||||
#endif
|
||||
#endif
|
||||
@ -1162,7 +1216,6 @@ namespace GAPI {
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#ifndef FFP
|
||||
bool GLES3 = false;
|
||||
#ifdef _OS_WEB
|
||||
@ -1173,7 +1226,7 @@ namespace GAPI {
|
||||
#if defined(__SDL2__)
|
||||
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &GLES_VERSION);
|
||||
#else
|
||||
#if defined(_OS_RPI) || defined(_OS_CLOVER)
|
||||
#if defined(_OS_RPI) || defined(_OS_CLOVER) || defined(_OS_GCW0)
|
||||
GLES_VERSION = 2;
|
||||
#else
|
||||
glGetIntegerv(GL_MAJOR_VERSION, &GLES_VERSION);
|
||||
@ -1191,9 +1244,11 @@ namespace GAPI {
|
||||
support.VAO = GLES3 || extSupport(ext, "_vertex_array_object");
|
||||
support.depthTexture = GLES3 || extSupport(ext, "_depth_texture");
|
||||
support.shadowSampler = _GL_EXT_shadow_samplers || _GL_ARB_shadow;
|
||||
support.discardFrame = extSupport(ext, "_discard_framebuffer");
|
||||
support.discardFrame = extSupport(ext, "_discard_framebuffer") && (glDiscardFramebufferEXT != NULL);
|
||||
support.texNPOT = GLES3 || extSupport(ext, "_texture_npot") || extSupport(ext, "_texture_non_power_of_two");
|
||||
support.texRG = GLES3 || extSupport(ext, "_texture_rg "); // hope that isn't last extension in string ;)
|
||||
support.texMaxLevel = GLES3 || extSupport(ext, "_texture_max_level");
|
||||
|
||||
#ifdef _GAPI_GLES2 // TODO
|
||||
support.shaderBinary = false;
|
||||
support.VAO = false;
|
||||
@ -1400,8 +1455,9 @@ namespace GAPI {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, defaultFBO);
|
||||
} else {
|
||||
GLenum texTarget = GL_TEXTURE_2D;
|
||||
if (target->opt & OPT_CUBEMAP)
|
||||
if (target->opt & OPT_CUBEMAP) {
|
||||
texTarget = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
|
||||
}
|
||||
|
||||
bool depth = target->fmt == FMT_DEPTH || target->fmt == FMT_SHADOW;
|
||||
|
||||
@ -1412,7 +1468,7 @@ namespace GAPI {
|
||||
glFramebufferRenderbuffer (GL_FRAMEBUFFER, depth ? GL_COLOR_ATTACHMENT0 : GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rtCache[!depth].items[rtIndex].ID);
|
||||
GLuint status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOG("status: %d\n", (int)status);
|
||||
LOG("status: 0x%04X\n", (int)status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1428,9 +1484,7 @@ namespace GAPI {
|
||||
#ifdef _OS_ANDROID
|
||||
glInvalidateFramebuffer(GL_FRAMEBUFFER, count, discard);
|
||||
#else
|
||||
#if !defined(__SDL2__) && !defined(_OS_PSC)
|
||||
glDiscardFramebufferEXT(GL_FRAMEBUFFER, count, discard);
|
||||
#endif
|
||||
glDiscardFramebufferEXT(GL_FRAMEBUFFER, count, discard);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
@ -133,11 +133,13 @@ static const OptionItem optDetail[] = {
|
||||
OptionItem( OptionItem::TYPE_PARAM, STR_OPT_DETAIL_SHADOWS, SETTINGS( detail.shadows ), STR_QUALITY_LOW, 0, 2 ),
|
||||
OptionItem( OptionItem::TYPE_PARAM, STR_OPT_DETAIL_WATER, SETTINGS( detail.water ), STR_QUALITY_LOW, 0, 2 ),
|
||||
OptionItem( OptionItem::TYPE_PARAM, STR_OPT_SIMPLE_ITEMS, SETTINGS( detail.simple ), STR_OFF, 0, 1 ),
|
||||
#if !defined(_OS_3DS) && !defined(_OS_GCW0)
|
||||
OptionItem( OptionItem::TYPE_PARAM, STR_OPT_RESOLUTION, SETTINGS( detail.scale ), STR_SCALE_100, 0, 3 ),
|
||||
#endif
|
||||
#if defined(_OS_WIN) || defined(_OS_LINUX) || defined(_OS_PSP) || defined(_OS_RPI) || defined(_OS_PSV)
|
||||
OptionItem( OptionItem::TYPE_PARAM, STR_OPT_DETAIL_VSYNC, SETTINGS( detail.vsync ), STR_OFF, 0, 1 ),
|
||||
#endif
|
||||
#if !defined(_OS_PSP) && !defined(_OS_PSV) && !defined(_OS_3DS)
|
||||
#if !defined(_OS_PSP) && !defined(_OS_PSV) && !defined(_OS_3DS) && !defined(_OS_GCW0)
|
||||
OptionItem( OptionItem::TYPE_PARAM, STR_OPT_DETAIL_STEREO, SETTINGS( detail.stereo ), STR_NO_STEREO, 0,
|
||||
#if defined(_OS_WIN) || defined(_OS_ANDROID)
|
||||
4 /* with VR option */
|
||||
@ -166,9 +168,12 @@ static const OptionItem optSound[] = {
|
||||
#define INV_GAMEPAD_ONLY
|
||||
#endif
|
||||
|
||||
#if defined(_OS_PSP) || defined(_OS_PSV) || defined(_OS_3DS)
|
||||
#if defined(_OS_PSP) || defined(_OS_PSV) || defined(_OS_3DS) || defined(_OS_GCW0)
|
||||
#define INV_SINGLE_PLAYER
|
||||
#define INV_GAMEPAD_ONLY
|
||||
#endif
|
||||
|
||||
#ifdef INV_SINGLE_PLAYER
|
||||
#define INV_CTRL_START_OPTION 1
|
||||
#else
|
||||
#define INV_CTRL_START_OPTION 2
|
||||
@ -1192,7 +1197,7 @@ struct Inventory {
|
||||
else if (Input::down[ikDown] || joy.down[jkDown] || joy.L.y > 0.5f)
|
||||
key = cDown;
|
||||
|
||||
#if defined(_OS_SWITCH) || defined(_OS_3DS)
|
||||
#if defined(_OS_SWITCH) || defined(_OS_3DS) || defined(_OS_GCW0)
|
||||
// swap A/B keys for Nintendo (Japanese) UX style
|
||||
if (Input::touchTimerVis == 0.0f) {
|
||||
if (key == cAction) {
|
||||
@ -1387,9 +1392,11 @@ struct Inventory {
|
||||
background[0] = NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < COUNT(background); i++)
|
||||
if (!background[i])
|
||||
background[i] = new Texture(INV_BG_SIZE, INV_BG_SIZE, 1, FMT_RGBA, OPT_TARGET);
|
||||
for (int i = 0; i < COUNT(background); i++) {
|
||||
if (!background[i]) {
|
||||
background[i] = new Texture(INV_BG_SIZE, INV_BG_SIZE, 1, FMT_RGB16, OPT_TARGET);
|
||||
}
|
||||
}
|
||||
|
||||
return background[view];
|
||||
}
|
||||
@ -2061,7 +2068,7 @@ struct Inventory {
|
||||
const char *bSelect = STR[STR_KEY_FIRST + ikEnter];
|
||||
const char *bBack = STR[STR_KEY_FIRST + Core::settings.controls[playerIndex].keys[cInventory].key];
|
||||
|
||||
#if defined(_OS_SWITCH) || defined(_OS_3DS)
|
||||
#if defined(_OS_SWITCH) || defined(_OS_3DS) || defined(_OS_GCW0)
|
||||
bSelect = "A";
|
||||
bBack = "B";
|
||||
#endif
|
||||
|
@ -1744,7 +1744,9 @@ struct Level : IGame {
|
||||
if (e.type == TR::Entity::CRYSTAL) {
|
||||
Crystal *c = (Crystal*)e.controller;
|
||||
renderEnvironment(c->getRoomIndex(), c->pos - vec3(0, 512, 0), &c->environment);
|
||||
c->environment->generateMipMap();
|
||||
#ifdef USE_CUBEMAP_MIPS
|
||||
c->environment->generateMipMap();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1565,6 +1565,8 @@ struct MeshBuilder {
|
||||
Core::mModel.identity();
|
||||
Core::mModel.setRot(basis.rot);
|
||||
Core::mModel.setPos(basis.pos);
|
||||
#else
|
||||
Core::active.shader->setParam(uBasis, *(vec4*)&basis, 2);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
@ -850,7 +850,11 @@ struct Crystal : Controller {
|
||||
Texture *environment;
|
||||
|
||||
Crystal(IGame *game, int entity) : Controller(game, entity) {
|
||||
environment = new Texture(64, 64, 1, FMT_RGBA, OPT_CUBEMAP | OPT_MIPMAPS | OPT_TARGET);
|
||||
uint32 opt = OPT_CUBEMAP | OPT_TARGET;
|
||||
#ifdef USE_CUBEMAP_MIPS
|
||||
opt |= OPT_MIPMAPS;
|
||||
#endif
|
||||
environment = new Texture(64, 64, 1, FMT_RGB16, opt);
|
||||
activate();
|
||||
}
|
||||
|
||||
|
3
src/platform/gcw0/build.sh
Executable file
3
src/platform/gcw0/build.sh
Executable file
@ -0,0 +1,3 @@
|
||||
set -e
|
||||
/opt/gcw0-toolchain/usr/bin/mipsel-gcw0-linux-uclibc-g++ -o OpenLara -D__GCW0__ -std=c++11 -O3 -s -g0 -mips32r2 -fno-exceptions -fno-rtti -ffunction-sections -fdata-sections -Wl,--gc-sections -Wno-invalid-source-encoding main.cpp ../../libs/stb_vorbis/stb_vorbis.c ../../libs/minimp3/minimp3.cpp ../../libs/tinf/tinflate.c -I/opt/vc/include -I../../ -L/opt/vc/lib/ -lGLESv2 -lEGL -lm -lrt -lpthread -lasound -ludev
|
||||
/opt/gcw0-toolchain/usr/bin/mipsel-gcw0-linux-uclibc-strip ../../../bin/OpenLara --strip-all --remove-section=.comment --remove-section=.note
|
BIN
src/platform/gcw0/icon.png
Normal file
BIN
src/platform/gcw0/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 546 B |
425
src/platform/gcw0/main.cpp
Normal file
425
src/platform/gcw0/main.cpp
Normal file
@ -0,0 +1,425 @@
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <linux/input.h>
|
||||
#include <linux/fb.h>
|
||||
#include <linux/vt.h>
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#include <pthread.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <libudev.h>
|
||||
#include <alsa/asoundlib.h>
|
||||
|
||||
#include "game.h"
|
||||
|
||||
#define WND_TITLE "OpenLara"
|
||||
|
||||
// timing
|
||||
unsigned int startTime;
|
||||
|
||||
int osGetTimeMS() {
|
||||
timeval t;
|
||||
gettimeofday(&t, NULL);
|
||||
return int((t.tv_sec - startTime) * 1000 + t.tv_usec / 1000);
|
||||
}
|
||||
|
||||
// sound
|
||||
snd_pcm_uframes_t SND_FRAMES = 512;
|
||||
snd_pcm_t *sndOut;
|
||||
Sound::Frame *sndData;
|
||||
pthread_t sndThread;
|
||||
|
||||
void* sndFill(void *arg) {
|
||||
while (sndOut) {
|
||||
Sound::fill(sndData, SND_FRAMES);
|
||||
|
||||
int count = SND_FRAMES;
|
||||
while (count > 0) {
|
||||
int frames = snd_pcm_writei(sndOut, &sndData[SND_FRAMES - count], count);
|
||||
if (frames < 0) {
|
||||
frames = snd_pcm_recover(sndOut, frames, 0);
|
||||
if (frames == -EAGAIN) {
|
||||
LOG("snd_pcm_writei try again\n");
|
||||
sleep(1);
|
||||
continue;
|
||||
}
|
||||
if (frames < 0) {
|
||||
LOG("snd_pcm_writei failed: %s\n", snd_strerror(frames));
|
||||
sndOut = NULL;
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
count -= frames;
|
||||
}
|
||||
|
||||
snd_pcm_prepare(sndOut);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sndInit() {
|
||||
unsigned int freq = 44100;
|
||||
|
||||
int err;
|
||||
|
||||
// In the perfect world ReedPlayer-Clover process
|
||||
// will release ALSA device before app running, but...
|
||||
for (int i = 0; i < 20; i++) { // 20 * 0.1 = 2 secs
|
||||
sndOut = NULL;
|
||||
if ((err = snd_pcm_open(&sndOut, "default", SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
|
||||
LOG("sound: try to snd_pcm_open #%d...\n", i);
|
||||
usleep(100000); // wait for 100 ms
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// I've bad news for you
|
||||
if (!sndOut) {
|
||||
LOG("! sound: snd_pcm_open %s\n", snd_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
snd_pcm_hw_params_t *params;
|
||||
|
||||
snd_pcm_hw_params_alloca(¶ms);
|
||||
snd_pcm_hw_params_any(sndOut, params);
|
||||
snd_pcm_hw_params_set_access(sndOut, params, SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||
|
||||
snd_pcm_hw_params_set_channels(sndOut, params, 2);
|
||||
snd_pcm_hw_params_set_format(sndOut, params, SND_PCM_FORMAT_S16_LE);
|
||||
snd_pcm_hw_params_set_rate_near(sndOut, params, &freq, NULL);
|
||||
|
||||
snd_pcm_hw_params_set_periods(sndOut, params, 4, 0);
|
||||
snd_pcm_hw_params_set_period_size_near(sndOut, params, &SND_FRAMES, NULL);
|
||||
snd_pcm_hw_params_get_period_size(params, &SND_FRAMES, 0);
|
||||
|
||||
snd_pcm_hw_params(sndOut, params);
|
||||
snd_pcm_prepare(sndOut);
|
||||
|
||||
sndData = new Sound::Frame[SND_FRAMES];
|
||||
memset(sndData, 0, SND_FRAMES * sizeof(Sound::Frame));
|
||||
if ((err = snd_pcm_writei(sndOut, sndData, SND_FRAMES)) < 0) {
|
||||
LOG("! sound: write %s\n", snd_strerror(err));
|
||||
sndOut = NULL;
|
||||
}
|
||||
|
||||
snd_pcm_start(sndOut);
|
||||
pthread_create(&sndThread, NULL, sndFill, NULL);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void sndFree() {
|
||||
pthread_cancel(sndThread);
|
||||
snd_pcm_drop(sndOut);
|
||||
snd_pcm_drain(sndOut);
|
||||
snd_pcm_close(sndOut);
|
||||
delete[] sndData;
|
||||
}
|
||||
|
||||
// Window
|
||||
struct FrameBuffer {
|
||||
unsigned short width;
|
||||
unsigned short height;
|
||||
} fb;
|
||||
|
||||
EGLDisplay display;
|
||||
EGLSurface surface;
|
||||
EGLContext context;
|
||||
|
||||
bool eglInit() {
|
||||
LOG("EGL init context...\n");
|
||||
|
||||
fb_var_screeninfo vinfo;
|
||||
int fd = open("/dev/fb0", O_RDWR);
|
||||
if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) < 0) {
|
||||
LOG("! can't get framebuffer size\n");
|
||||
return false;
|
||||
}
|
||||
close(fd);
|
||||
|
||||
fb.width = vinfo.xres;
|
||||
fb.height = vinfo.yres;
|
||||
|
||||
const EGLint eglAttr[] = {
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
|
||||
EGL_BLUE_SIZE, 5,
|
||||
EGL_GREEN_SIZE, 6,
|
||||
EGL_RED_SIZE, 5,
|
||||
EGL_DEPTH_SIZE, 16,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
const EGLint ctxAttr[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL_NONE
|
||||
};
|
||||
|
||||
display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (display == EGL_NO_DISPLAY) {
|
||||
LOG("eglGetDisplay = EGL_NO_DISPLAY\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eglInitialize(display, NULL, NULL) == EGL_FALSE) {
|
||||
LOG("eglInitialize = EGL_FALSE\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
EGLConfig config;
|
||||
EGLint configCount;
|
||||
|
||||
if (eglChooseConfig(display, eglAttr, &config, 1, &configCount) == EGL_FALSE || configCount == 0) {
|
||||
LOG("eglChooseConfig = EGL_FALSE\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
surface = eglCreateWindowSurface(display, config, 0, NULL);
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
LOG("eglCreateWindowSurface = EGL_NO_SURFACE\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
|
||||
if (context == EGL_NO_CONTEXT) {
|
||||
LOG("eglCreateContext = EGL_NO_CONTEXT\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
|
||||
LOG("eglMakeCurrent = EGL_FALSE\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void eglFree() {
|
||||
LOG("EGL release context\n");
|
||||
eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglDestroySurface(display, surface);
|
||||
eglDestroyContext(display, context);
|
||||
eglTerminate(display);
|
||||
}
|
||||
|
||||
// Input
|
||||
#define MAX_INPUT_DEVICES 16
|
||||
struct InputDevice {
|
||||
int fd;
|
||||
} inputDevices[MAX_INPUT_DEVICES];
|
||||
|
||||
udev *udevObj;
|
||||
udev_monitor *udevMon;
|
||||
int udevMon_fd;
|
||||
|
||||
vec2 joyL, joyR;
|
||||
|
||||
bool osJoyReady(int index) {
|
||||
return index == 0; // TODO
|
||||
}
|
||||
|
||||
void osJoyVibrate(int index, float L, float R) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
JoyKey codeToJoyKey(int code) {
|
||||
switch (code) {
|
||||
// gamepad
|
||||
case KEY_LEFT : return jkLeft;
|
||||
case KEY_RIGHT : return jkRight;
|
||||
case KEY_UP : return jkUp;
|
||||
case KEY_DOWN : return jkDown;
|
||||
case KEY_LEFTCTRL : return jkB;
|
||||
case KEY_LEFTALT : return jkA;
|
||||
case KEY_SPACE : return jkY;
|
||||
case KEY_LEFTSHIFT : return jkX;
|
||||
case KEY_TAB : return jkLB;
|
||||
case KEY_BACKSPACE : return jkRB;
|
||||
case KEY_ESC : return jkSelect;
|
||||
case KEY_ENTER : return jkStart;
|
||||
case KEY_KPSLASH : return jkL;
|
||||
case KEY_KPDOT : return jkR;
|
||||
case KEY_PAGEUP : return jkLT;
|
||||
case KEY_PAGEDOWN : return jkRT;
|
||||
case KEY_POWER : Core::quit();
|
||||
}
|
||||
return jkNone;
|
||||
}
|
||||
|
||||
int inputDevIndex(const char *node) {
|
||||
const char *str = strstr(node, "/event");
|
||||
if (str)
|
||||
return atoi(str + 6);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void inputDevAdd(const char *node, udev_device *device) {
|
||||
int index = inputDevIndex(node);
|
||||
if (index != -1) {
|
||||
InputDevice &item = inputDevices[index];
|
||||
item.fd = open(node, O_RDONLY | O_NONBLOCK);
|
||||
//ioctl(item.fd, EVIOCGRAB, 1);
|
||||
//LOG("input: add %s (%d)\n", node, item.joyIndex);
|
||||
}
|
||||
}
|
||||
|
||||
bool inputInit() {
|
||||
joyL = joyR = vec2(0);
|
||||
|
||||
for (int i = 0; i < MAX_INPUT_DEVICES; i++) {
|
||||
inputDevices[i].fd = -1;
|
||||
}
|
||||
|
||||
udevObj = udev_new();
|
||||
if (!udevObj)
|
||||
return false;
|
||||
|
||||
udevMon = udev_monitor_new_from_netlink(udevObj, "udev");
|
||||
udev_monitor_filter_add_match_subsystem_devtype(udevMon, "input", NULL);
|
||||
udev_monitor_enable_receiving(udevMon);
|
||||
udevMon_fd = udev_monitor_get_fd(udevMon);
|
||||
|
||||
udev_enumerate *e = udev_enumerate_new(udevObj);
|
||||
udev_enumerate_add_match_subsystem(e, "input");
|
||||
udev_enumerate_scan_devices(e);
|
||||
udev_list_entry *devices = udev_enumerate_get_list_entry(e);
|
||||
|
||||
udev_list_entry *entry;
|
||||
udev_list_entry_foreach(entry, devices) {
|
||||
const char *path, *node;
|
||||
udev_device *device;
|
||||
|
||||
path = udev_list_entry_get_name(entry);
|
||||
device = udev_device_new_from_syspath(udevObj, path);
|
||||
node = udev_device_get_devnode(device);
|
||||
|
||||
if (node)
|
||||
inputDevAdd(node, device);
|
||||
}
|
||||
udev_enumerate_unref(e);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void inputFree() {
|
||||
for (int i = 0; i < MAX_INPUT_DEVICES; i++)
|
||||
if (inputDevices[i].fd != -1)
|
||||
close(inputDevices[i].fd);
|
||||
udev_monitor_unref(udevMon);
|
||||
udev_unref(udevObj);
|
||||
}
|
||||
|
||||
float joyAxisValue(int value) {
|
||||
return value / 1536.0f - 1.0f;
|
||||
}
|
||||
|
||||
vec2 joyDir(const vec2 &value) {
|
||||
float dist = min(1.0f, value.length());
|
||||
return value.normal() * dist;
|
||||
}
|
||||
|
||||
void inputUpdate() {
|
||||
// get input events
|
||||
input_event events[16];
|
||||
|
||||
for (int i = 0; i < MAX_INPUT_DEVICES; i++) {
|
||||
if (inputDevices[i].fd == -1) continue;
|
||||
int rb = read(inputDevices[i].fd, events, sizeof(events));
|
||||
|
||||
input_event *e = events;
|
||||
while (rb > 0) {
|
||||
switch (e->type) {
|
||||
case EV_KEY : {
|
||||
JoyKey key = codeToJoyKey(e->code);
|
||||
Input::setJoyDown(0, key, e->value != 0);
|
||||
break;
|
||||
}
|
||||
case EV_ABS : {
|
||||
switch (e->code) {
|
||||
// Left stick
|
||||
case ABS_X : joyL.x = -joyAxisValue(e->value); break;
|
||||
case ABS_Y : joyL.y = -joyAxisValue(e->value); break;
|
||||
// Right stick
|
||||
case ABS_RX : joyR.x = joyAxisValue(e->value); break;
|
||||
case ABS_RY : joyR.y = joyAxisValue(e->value); break;
|
||||
}
|
||||
|
||||
Input::setJoyPos(0, jkL, joyDir(joyL));
|
||||
Input::setJoyPos(0, jkR, joyDir(joyR));
|
||||
}
|
||||
}
|
||||
e++;
|
||||
rb -= sizeof(events[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (!eglInit()) {
|
||||
LOG("! can't initialize EGL context\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
Core::width = fb.width;
|
||||
Core::height = fb.height;
|
||||
|
||||
cacheDir[0] = saveDir[0] = contentDir[0] = 0;
|
||||
|
||||
const char *home;
|
||||
if (!(home = getenv("HOME")))
|
||||
home = getpwuid(getuid())->pw_dir;
|
||||
strcpy(contentDir, home);
|
||||
strcat(contentDir, "/.openlara/");
|
||||
|
||||
LOG("content dir: %s\n", contentDir);
|
||||
|
||||
struct stat st = {0};
|
||||
|
||||
if (stat(contentDir, &st) == -1) {
|
||||
LOG("no data directory found, please copy the original game content into /home/.openlara/\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
strcpy(saveDir, contentDir);
|
||||
|
||||
strcpy(cacheDir, contentDir);
|
||||
strcat(cacheDir, "cache/");
|
||||
|
||||
if (stat(cacheDir, &st) == -1 && mkdir(cacheDir, 0777) == -1) {
|
||||
cacheDir[0] = 0;
|
||||
LOG("can't create /home/.openlara/cache/\n");
|
||||
}
|
||||
|
||||
timeval t;
|
||||
gettimeofday(&t, NULL);
|
||||
startTime = t.tv_sec;
|
||||
|
||||
Game::init();
|
||||
inputInit();
|
||||
sndInit();
|
||||
|
||||
while (!Core::isQuit) {
|
||||
inputUpdate();
|
||||
|
||||
if (Game::update()) {
|
||||
Game::render();
|
||||
Core::waitVBlank();
|
||||
eglSwapBuffers(display, surface);
|
||||
} else
|
||||
usleep(9000);
|
||||
};
|
||||
|
||||
inputFree();
|
||||
|
||||
Game::deinit();
|
||||
eglFree();
|
||||
|
||||
sndFree();
|
||||
|
||||
return 0;
|
||||
}
|
30
src/platform/gcw0/make_opk.sh
Executable file
30
src/platform/gcw0/make_opk.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
|
||||
OPK_NAME=OpenLara.opk
|
||||
|
||||
echo ${OPK_NAME}
|
||||
|
||||
# create default.gcw0.desktop
|
||||
cat > default.gcw0.desktop <<EOF
|
||||
[Desktop Entry]
|
||||
Name=OpenLara
|
||||
Comment=Classic Tomb Raider open-source engine
|
||||
Exec=OpenLara
|
||||
Terminal=false
|
||||
Type=Application
|
||||
StartupNotify=true
|
||||
Icon=icon
|
||||
Categories=games;
|
||||
X-OD-NeedsDownscaling=false
|
||||
EOF
|
||||
|
||||
# create opk
|
||||
FLIST="OpenLara"
|
||||
FLIST="${FLIST} default.gcw0.desktop"
|
||||
FLIST="${FLIST} icon.png"
|
||||
|
||||
rm -f ${OPK_NAME}
|
||||
mksquashfs ${FLIST} ${OPK_NAME} -all-root -no-xattrs -noappend -no-exports
|
||||
|
||||
cat default.gcw0.desktop
|
||||
rm -f default.gcw0.desktop
|
@ -13,7 +13,7 @@ R"====(
|
||||
#endif
|
||||
|
||||
#ifdef OPT_SHADOW
|
||||
#define SHADOW_TEXEL vec3(1.0 / 2048.0, 1.0 / 2048.0, 0.0)
|
||||
#define SHADOW_TEXEL vec3(1.0 / SHADOW_SIZE, 1.0 / SHADOW_SIZE, 0.0)
|
||||
uniform mat4 uLightProj;
|
||||
|
||||
#ifdef OPT_VLIGHTPROJ
|
||||
@ -95,7 +95,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction
|
||||
}
|
||||
|
||||
vec4 _transform() {
|
||||
#if defined(TYPE_ENTITY) || defined(TYPE_MIRROR)
|
||||
#if (defined(TYPE_ENTITY) || defined(TYPE_MIRROR)) && defined(MESH_SKINNING)
|
||||
int index = int(aCoord.w * 2.0);
|
||||
vec4 rBasisRot = uBasis[index];
|
||||
vec4 rBasisPos = uBasis[index + 1];
|
||||
@ -122,18 +122,16 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction
|
||||
|
||||
float fog;
|
||||
#if defined(UNDERWATER) && !defined(OPT_UNDERWATER_FOG)
|
||||
float d;
|
||||
if (uViewPos.y < uParam.y) // TODO: fix for mediump
|
||||
d = abs((coord.y - uParam.y) / normalize(uViewPos.xyz - coord).y);
|
||||
else
|
||||
d = length(uViewPos.xyz - coord);
|
||||
float d = length(uViewPos.xyz - coord);
|
||||
if (uViewPos.y < uParam.y) {
|
||||
d *= (coord.y - uParam.y) / (coord.y - uViewPos.y);
|
||||
}
|
||||
fog = d * WATER_FOG_DIST;
|
||||
fog *= step(uParam.y, coord.y);
|
||||
vNormal.w = clamp(1.0 / exp(fog), 0.0, 1.0);
|
||||
#else
|
||||
fog = length(vViewVec.xyz);
|
||||
vNormal.w = clamp(1.0 / exp(fog), 0.0, 1.0);
|
||||
#endif
|
||||
vNormal.w = clamp(1.0 / exp(fog), 0.0, 1.0);
|
||||
|
||||
vCoord = coord;
|
||||
#endif
|
||||
@ -189,7 +187,7 @@ varying vec4 vTexCoord; // xy - atlas coords, zw - trapezoidal correction
|
||||
lum.w = dot(vNormal.xyz, normalize(lv3)); att.w = dot(lv3, lv3);
|
||||
vec4 light = max(vec4(0.0), lum) * max(vec4(0.0), vec4(1.0) - att);
|
||||
|
||||
#if (defined(TYPE_ENTITY) || defined(TYPE_ROOM)) && defined(UNDERWATER)
|
||||
#if (defined(TYPE_ENTITY) || defined(TYPE_ROOM)) && defined(UNDERWATER) && defined(VERT_CAUSTICS)
|
||||
light.x *= 0.5 + abs(sin(dot(coord.xyz, vec3(1.0 / 1024.0)) + uParam.x)) * 0.75;
|
||||
#endif
|
||||
|
||||
|
@ -22,9 +22,14 @@ R"====(
|
||||
}
|
||||
|
||||
void main() {
|
||||
int index = int(aCoord.w * 2.0);
|
||||
vec4 rBasisRot = uBasis[index];
|
||||
vec4 rBasisPos = uBasis[index + 1];
|
||||
#ifdef MESH_SKINNING
|
||||
int index = int(aCoord.w * 2.0);
|
||||
vec4 rBasisRot = uBasis[index];
|
||||
vec4 rBasisPos = uBasis[index + 1];
|
||||
#else
|
||||
vec4 rBasisRot = uBasis[0];
|
||||
vec4 rBasisPos = uBasis[1];
|
||||
#endif
|
||||
#ifdef ALPHA_TEST
|
||||
vTexCoord = aTexCoord.xy;
|
||||
#endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user