Browse Source

move glfw/opengl specific items out

master
Odilitime 5 years ago
parent
commit
dbd1cc6024
  1. 448
      src/interfaces/graphical/renderers/renderer.cpp
  2. 142
      src/interfaces/graphical/renderers/renderer.h

448
src/interfaces/graphical/renderers/renderer.cpp

@ -1,448 +1,2 @@ @@ -1,448 +1,2 @@
#include "opengl.h"
#include "renderer.h"
#include <iostream>
#include <cmath>
#include <cstring> // for memset
// maybe change to return std::pair<GLuint, GLuint>
bool initGL() {
const GLubyte *renderer = glGetString(GL_RENDERER);
const GLubyte *version = glGetString(GL_VERSION);
std::cout << "Renderer: " << renderer << std::endl;
std::cout << "Version: " << version << std::endl;
GLenum glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::initGL - start - not ok: " << glErr << std::endl;
}
glEnable(GL_BLEND);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::initGL - glEnable blend - not ok: " << glErr << std::endl;
}
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::initGL - glBlendFunc - not ok: " << glErr << std::endl;
}
glClearColor(0.8f, 0.8f, 0.8f, 0.8f);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::initGL - glClearColor - not ok: " << glErr << std::endl;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::initGL - glTexParameteri min - not ok: " << glErr << std::endl;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::initGL - glTexParameteri mag - not ok: " << glErr << std::endl;
}
//std::cout << "OpenGL is set up" << std::endl;
return true;
}
bool initGLEW() {
glewExperimental = GL_TRUE;
const GLenum err = glewInit();
if (err != GLEW_OK) {
std::cout << "Could not initialize GLEW: " << glewGetErrorString(err) << std::endl;
return false;
}
return true;
}
// only called once
bool opengl::initialize() {
if (!glfwInit()) {
std::cout << "Could not initialize GLFW" << std::endl;
return false;
}
cursorHand = glfwCreateStandardCursor(GLFW_HAND_CURSOR);
cursorArrow = glfwCreateStandardCursor(GLFW_ARROW_CURSOR);
cursorIbeam = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR);
glfwSetErrorCallback([](int error, const char* description) {
std::cout << "glfw error [" << error << "]:" << description << std::endl;
});
// I don't think you can init GLEW without a window
return true;
}
opengl_window_handle * opengl::createWindow(std::string title, Rect *position, unsigned int flags) {
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
int windowWidth = position->w;
int windowHeight = position->h;
opengl_window_handle *win = new opengl_window_handle();
win->width = position->w;
win->height = position->h;
GLFWwindow *window = glfwCreateWindow(windowWidth, windowHeight, title.c_str(), nullptr, nullptr);
win->window = window;
// replace first parameter of all these callbacks with our window object instead of a GLFWwindow
glfwSetWindowUserPointer(window, this);
// probably should set up all the hooks here
glfwMakeContextCurrent(window);
// init GLEW
if (!initGLEW()) {
// FIXME: un do glfw create window
// can't delete without a non-virtual deconstrcutor
//delete win;
return nullptr;
}
// init shaders?
// we only want the box (texture shader)
// I don't think you can init GLEW without a window
initGL();
// load the shaders
win->textureShader = win->shaderLoader.getShader(VertexShader("TextureShader.vert"),
FragmentShader("TextureShader.frag"));
win->fontShader = win->shaderLoader.getShader(VertexShader("FontShader.vert"),
FragmentShader("FontShader.frag"));
// configure opengl coordinates
//std::cout << "position: " << position->w << "x" << position->h << std::endl;
glViewport(0, 0, static_cast<GLsizei>(position->w), static_cast<GLsizei>(position->h));
GLenum glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glViewport - not ok: " << gluErrorString(glErr) << std::endl;
}
/*
// Change to the projection matrix, reset the matrix and set up orthagonal projection (i.e. 2D)
glMatrixMode(GL_PROJECTION);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glMatrixMode - not ok: " << gluErrorString(glErr) << std::endl;
}
glLoadIdentity();
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glLoadIdentity - not ok: " << gluErrorString(glErr) << std::endl;
}
glOrtho(0, position->w, position->h, 0, 0, 1); // Paramters: left, right, bottom, top, near, far
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glOrtho - not ok: " << gluErrorString(glErr) << std::endl;
}
*/
// set up box shader
GLuint elementBufferObject = 0;
glGenVertexArrays(1, &win->vertexArrayObject);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - genVAO - not ok: " << glErr << std::endl;
}
glBindVertexArray(win->vertexArrayObject);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - bindVAO - not ok: " << glErr << std::endl;
}
glGenBuffers(1, &elementBufferObject);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - genEBO - not ok: " << glErr << std::endl;
}
// we may need the VBO here...
glGenBuffers(1, &win->vertexBufferObject);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - genVBO - not ok: " << glErr << std::endl;
}
float vertices[20] = {
0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f
};
glBindBuffer(GL_ARRAY_BUFFER, win->vertexBufferObject); // selects buffer
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - bindVBO - not ok: " << glErr << std::endl;
}
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // writes buffer
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - bufVBO - not ok: " << glErr << std::endl;
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementBufferObject);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - bindEBO - not ok: " << glErr << std::endl;
}
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(this->indices), this->indices, GL_STATIC_DRAW);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - buffEBO - not ok: " << glErr << std::endl;
}
// attribute 0 = 3 floats
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), nullptr);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glVertexAttribPointer0 - not ok: " << glErr << std::endl;
}
glEnableVertexAttribArray(0);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glEnableVertexAttribArray0 - not ok: " << glErr << std::endl;
}
// attribute 1 = 2 floats, at 3
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), reinterpret_cast<void*>(3 * sizeof(float)));
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glVertexAttribPointer1 - not ok: " << glErr << std::endl;
}
glEnableVertexAttribArray(1);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createWindow - glEnableVertexAttribArray1 - not ok: " << glErr << std::endl;
}
//glBindVertexArray(0);
return win;
}
void opengl_window_handle::clear() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
GLenum glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::clear - not ok: " << glErr << std::endl;
}
}
void opengl_window_handle::swapBuffers() {
glfwSwapBuffers(this->window);
GLenum glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::swapBuffers - not ok: " << glErr << std::endl;
}
}
opengl_texture_handle* opengl_window_handle::createTexture(unsigned char* texture, GLsizei w, GLsizei h) {
opengl_texture_handle *handle = new opengl_texture_handle;
glGenTextures(1, &handle->number);
// make sure texture size is a power of two
size_t potw = pow(2, ceil(log(w) / log(2)));
size_t poth = pow(2, ceil(log(h) / log(2)));
// FIXME: need to dynamically allocate for c++
//unsigned char data[16384][16384][4];
//unsigned char ***image = allocate_dynamic_bitmap(poth, potw, 4);
unsigned char *data = (unsigned char *)malloc(sizeof(unsigned char) * poth * potw * 4);
//memset(data, 0, poth * potw * 4);
//memset(image, 0, poth * potw * 4);
// unflip texture
unsigned int loadWidth = static_cast<unsigned int>(w);
unsigned int loadHeight = static_cast<unsigned int>(h);
for (unsigned int py = 0; py < loadHeight; py++) {
for (unsigned int px = 0; px < loadWidth; px++) {
for (unsigned int i = 0; i < 4; i++) {
//data[1023 - py][px][i] = anime.pixel_data[((px * 4) + (py * 4 * 1024)) + i];
size_t read = ((px * 4) + (py * 4 * loadWidth)) + i;
unsigned char val = texture[read];
size_t ypos = potw - 1 - py; // flip y
size_t write = ((px * 4) + (ypos * 4 * potw)) + i;
data[write] = val;
//data[ypos][px][i] = val;
}
}
}
// upload
glBindTexture(GL_TEXTURE_2D, handle->number); // select
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<int>(potw), static_cast<int>(poth), 0, GL_RGBA, GL_UNSIGNED_BYTE, data); // upload
glGenerateMipmap(GL_TEXTURE_2D); // process it
glBindTexture(GL_TEXTURE_2D, 0); // unselect
return handle;
}
opengl_texture_handle* opengl_window_handle::createTextureFromColor(const unsigned int hexColor) {
//std::cout << "createTextureFromColor: " << hexColor << std::endl;
unsigned char texture[1][1][4];
texture[0][0][0]=(hexColor >> 24) & 0xFF;
texture[0][0][1]=(hexColor >> 16) & 0xFF;
texture[0][0][2]=(hexColor >> 8) & 0xFF;
texture[0][0][3]=(hexColor >> 0) & 0xFF;
//std::cout << "opengl::createTextureFromColor R: " << (int)texture[0][0][0] << " G: " << (int)texture[0][0][1] << " B: " << (int)texture[0][0][2] << " A: " << (int)texture[0][0][3] << std::endl;
// I can't figure out how to pass data to opengl::createTexture
// so we'll just do this for now
opengl_texture_handle *handle=new opengl_texture_handle;
glGenTextures(1, &handle->number);
GLenum glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createTextureFromColor - glGenTextures - not ok: " << gluErrorString(glErr) << std::endl;
}
glBindTexture(GL_TEXTURE_2D, handle->number); // select
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createTextureFromColor - bindTexture - not ok: " << glErr << std::endl;
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture); // upload
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createTextureFromColor - glTexImage2D - not ok: " << glErr << std::endl;
}
glGenerateMipmap(GL_TEXTURE_2D); // process it
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createTextureFromColor - glGenerateMipmap - not ok: " << glErr << std::endl;
}
glBindTexture(GL_TEXTURE_2D, 0); // unselect
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "opengl::createTextureFromColor - unBindTex - not ok: " << glErr << std::endl;
}
return handle;
}
#include <cmath>
// converts 0-1 to screen
// but centered
void pointToViewport(float &rawX, float &rawY, size_t windowWidth, size_t windowHeight) {
//if (useBoxShader) {
//std::cout << "Component::pointToViewport - notBoundToPage converting from " << static_cast<int>(rawX) << "," << static_cast<int>(rawY) << std::endl;
//std::cout << "BoundToPage using " << screenWidth << "x" << screenHeight << std::endl;
rawX = ((rawX / windowWidth) * 2) - 1;
rawY = ((rawY / windowHeight) * 2) - 1;
//std::cout << "Component::pointToViewport - BoundToPage using " << static_cast<int>(rawX) << "x" << static_cast<int>(rawY) << std::endl;
//std::cout << "Component::pointToViewport - BoundToPage converted to " << rawX << "," << rawY << std::endl;
/*
} else {
//std::cout << "notBoundToPage using " << screenWidth << "x" << screenHeight << std::endl;
//std::cout << "Component::pointToViewport - notBoundToPage converting from " << static_cast<int>(rawX) << "," << static_cast<int>(rawY) << std::endl;
if (rawX < 0) {
rawX += windowWidth;
}
if (rawY < 0) {
rawY += windowHeight;
}
if (rawX > 1) {
rawX /= windowWidth;
}
if (rawY > 1) {
rawY /= windowHeight;
}
rawX = (rawX * 2) - 1;
rawY = (rawY * 2) - 1;
//std::cout << "Component::pointToViewport - notBoundToPage converted to " << rawX << "," << rawY << std::endl;
}
*/
}
// keeps 0-1 (and *2 to convert to screen)
// but also takes pixels (and converts to screen)
// anchors to upperleft
void distanceToViewport(float &rawX, float &rawY, size_t windowWidth, size_t windowHeight) {
if (std::abs(rawX) > 1) {
rawX /= windowWidth;
}
if (std::abs(rawY) > 1) {
rawY /= windowHeight;
}
rawX *= 2;
rawY *= 2;
}
void opengl_window_handle::drawTexturedBox(opengl_texture_handle *texture, Rect *position) {
std::cout << "Window size: " << this->width << "," << this->height << std::endl;
// figure out new vertices
float vx = position->x;
// 320, 320 is mid to top
// 0, 320 to bottom to top
//float vy = (int)position->h - (int)position->y - (int)this->height;
float vy = static_cast<int>(position->h) - position->y;
float vWidth = position->w;
float vHeight = position->h;
//std::cout << "placing box at " << (int)vx << "x" << (int)vy << " size: " << (int)vWidth << "x" << (int)vHeight << std::endl;
pointToViewport(vx, vy, this->width, this->height);
//std::cout << "vWidth before: " << (int)vWidth << std::endl;
distanceToViewport(vWidth, vHeight, this->width, this->height);
//std::cout << "vWidth after: " << (int)vWidth << std::endl;
//std::cout << "placing box at GL " << (int)vx << "x" << (int)vy << " size: " << (int)(vWidth*10000) << "x" << (int)(vHeight*10000) << std::endl;
float vertices[20] = {
0.0f, 0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f
};
vertices[(0 * 5) + 0] = vx;
vertices[(0 * 5) + 1] = vy + vHeight;
vertices[(1 * 5) + 0] = vx + vWidth;
vertices[(1 * 5) + 1] = vy + vHeight;
vertices[(2 * 5) + 0] = vx + vWidth;
vertices[(2 * 5) + 1] = vy;
vertices[(3 * 5) + 0] = vx;
vertices[(3 * 5) + 1] = vy;
glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
GLenum glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "BoxComponent::render - glBindBuffer not ok: " << glErr << std::endl;
}
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "BoxComponent::render - glBufferData not ok: " << glErr << std::endl;
}
float textureTransformMatrix[16] = {
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
};
textureShader->bind();
GLint transformLocation = textureShader->uniform("transform");
glUniformMatrix4fv(transformLocation, 1, GL_FALSE, textureTransformMatrix);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "MultiComponent::render - glUniformMatrix4fv - not ok: " << glErr << std::endl;
}
glBindVertexArray(vertexArrayObject);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "BoxComponent::render - glBindVertexArray not ok: " << glErr << std::endl;
}
glBindTexture(GL_TEXTURE_2D, texture->number);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "BoxComponent::render - glBindTexture not ok: " << glErr << std::endl;
}
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glErr=glGetError();
if(glErr != GL_NO_ERROR) {
std::cout << "BoxComponent::render - glDrawElements not ok: " << glErr << std::endl;
}
}

142
src/interfaces/graphical/renderers/renderer.h

@ -1,12 +1,10 @@ @@ -1,12 +1,10 @@
#ifndef OPENGL_H
#define OPENGL_H
#ifndef RENDERER_H
#define RENDERER_H
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <memory> // for unique_ptr
#include <vector>
#include <string>
#include "ShaderLoader.h"
#include <functional>
#ifdef FLOATCOORDS
typedef double coordinates;
@ -16,132 +14,52 @@ typedef short coordinates; @@ -16,132 +14,52 @@ typedef short coordinates;
typedef unsigned short sizes;
#endif
struct textureMap {
// was GLfloat
float map[4];
};
typedef struct{
coordinates x, y;
sizes w, h;
} Rect;
class texture_handle {
class Sprite {
};
class window_handle {
class WindowHandle {
public:
virtual void clear() { }
virtual void swapBuffers() { }
// can we const texture?
virtual texture_handle* createTexture(unsigned char *texture, GLsizei w, GLsizei h) { return nullptr; }
virtual texture_handle* createTextureFromColor(const unsigned int hexColor) { return nullptr; }
virtual void drawTexturedBox(texture_handle *texture, Rect *position) {}
virtual Sprite* createSprite(unsigned char *texture, int w, int h) { return nullptr; }
virtual Sprite* createTextSprite(unsigned char *texture, int w, int h, textureMap &textureMap) { return nullptr; }
virtual Sprite* createSpriteFromColor(const unsigned int hexColor) { return nullptr; }
virtual void drawSpriteBox(Sprite *texture, Rect *position) {}
virtual void drawSpriteText(Sprite *texture, unsigned int hexColor, Rect *position) {}
//
size_t width;
size_t height;
// window functions
std::function<void(int w, int h)> onResize = nullptr;
// mouse functions
std::function<void(int x, int y)> onMouseDown = nullptr;
std::function<void(int x, int y)> onMouseUp = nullptr;
std::function<void(int x, int y)> onMouseMove = nullptr;
std::function<void(int x, int y)> onWheel = nullptr;
// keyboard functions
std::function<void(int key, int scancode, int mods)> onKeyUp = nullptr;
std::function<void(int key, int scancode, int mods)> onKeyRepeat = nullptr;
std::function<void(int key, int scancode, int mods)> onKeyDown = nullptr;
std::function<void(unsigned int codepoint)> onKeyPress = nullptr;
};
// maybe convert to interface?
class baseRenderer {
class BaseRenderer {
public:
virtual bool initialize() { return true; }
// maybe it should return a window object?
virtual window_handle *createWindow(std::string title, Rect *position, unsigned int flags) { return nullptr; }
};
struct quadVector {
GLfloat vector[4][3];
};
class texture_atlas {
public:
GLuint texture;
sizes w, h; // texture width/height
sizes mx, my; // max x,y currently available
std::vector<Rect*> used; // small struct to loop on vs. unused
std::vector<Rect*> available; // have to generate it, might as well cache it
bool smooth;
unsigned char bpp; // 0-4
~texture_atlas();
void updateMaxAvailArea();
void markAreaUsed(Rect *it);
void rebuildAvailable();
};
class opengl_texture_handle : public texture_handle {
public:
// future
texture_atlas *mp_atlas;
quadVector m_texureVector;
// temp for now
GLuint number;
float s0;
float t0;
float s1;
float t1;
virtual WindowHandle *createWindow(std::string title, Rect *position, unsigned int flags) { return nullptr; }
};
// unlike a texture more like a sprite
class OpenglTexturedQuad {
public:
bool fontShader;
sizes width;
sizes height;
// vertices
coordinates x0;
coordinates y0;
coordinates x1;
coordinates y1;
// texture map
float s0;
float t0;
float s1;
float t1;
sizes textureWidth;
sizes textureHeight;
std::unique_ptr<unsigned char[]> textureData;
};
class opengl; // few declr
bool initGL();
bool initGLEW();
class opengl_window_handle : public window_handle {
public:
void clear();
void swapBuffers();
opengl_texture_handle* createTexture(unsigned char* texture, GLsizei w, GLsizei h);
opengl_texture_handle* createTextureFromColor(const unsigned int hexColor);
using window_handle::drawTexturedBox;
void drawTexturedBox(opengl_texture_handle *texture, Rect *position);
// property
GLFWwindow *window;
opengl *renderer;
// our singular vao
GLuint vertexArrayObject = 0;
// our singular vbo
GLuint vertexBufferObject = 0;
ShaderLoader shaderLoader;
Shader *textureShader;
Shader *fontShader;
};
// singleton, there should only be one of these in the system
class opengl : public baseRenderer {
public:
bool initialize();
opengl_window_handle *createWindow(std::string title, Rect *position, unsigned int flags);
// need texture/sprite functions (for elements that are going to be used across windows)
// maybe take in opengl_window_handle*, nullptr meaning all
// properties
const unsigned int indices[6] = {
0, 1, 2,
0, 2, 3
};
GLFWcursor* cursorHand;
GLFWcursor* cursorArrow;
GLFWcursor* cursorIbeam;
};
void pointToViewport(float&, float&, size_t, size_t);
void distanceToViewport(float&, float&, size_t, size_t);
#endif

Loading…
Cancel
Save