diff --git a/CMakeLists.txt b/CMakeLists.txt index e08682e..bba4aaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,9 @@ set(Basics Camera Shader Text - Texture + Texture_Basics + Texture_Compression + Texture_VirtualTexturing Transformation ) diff --git a/samples/Basics/README.md b/samples/Basics/README.md index 814dc8d..d05de4e 100644 --- a/samples/Basics/README.md +++ b/samples/Basics/README.md @@ -1,8 +1,11 @@ # Basic Examples Basic OpenGL Implementations for: + - Transformation - Shader - Coordinate System -- Texture -- Camera \ No newline at end of file +- Texture Basics +- Texture Compression +- Virtual Texturing +- Camera diff --git a/samples/Basics/Texture/shader/shader.vert b/samples/Basics/Texture/shader/shader.vert deleted file mode 100644 index feb3bfc..0000000 --- a/samples/Basics/Texture/shader/shader.vert +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 core -layout (location = 0) in vec3 aPos; -layout (location = 1) in vec3 aColor; - -out vec3 ourColor; - -void main() -{ - gl_Position = vec4(aPos, 1.0); - ourColor = aColor; -} \ No newline at end of file diff --git a/samples/Basics/Texture/shader/shaderfs1.frag b/samples/Basics/Texture/shader/shaderfs1.frag deleted file mode 100644 index bde012b..0000000 --- a/samples/Basics/Texture/shader/shaderfs1.frag +++ /dev/null @@ -1,9 +0,0 @@ -#version 330 core -out vec4 FragColor; - -in vec3 ourColor; - -void main() -{ - FragColor = vec4(ourColor, 1.0f); -} \ No newline at end of file diff --git a/samples/Basics/Texture/shader/shaderfs2.frag b/samples/Basics/Texture/shader/shaderfs2.frag deleted file mode 100644 index a769230..0000000 --- a/samples/Basics/Texture/shader/shaderfs2.frag +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 core -out vec4 FragColor; - -in vec3 ourColor; -uniform vec4 ourColor2; // we set this variable in the OpenGL code. - -void main() -{ - //FragColor = vec4(ourColor, 1.0f); - FragColor = vec4(1.0, 1.0f, 1.0f , 1.0f); -} \ No newline at end of file diff --git a/samples/Basics/Texture/shader/shaderfs3.frag b/samples/Basics/Texture/shader/shaderfs3.frag deleted file mode 100644 index 890906a..0000000 --- a/samples/Basics/Texture/shader/shaderfs3.frag +++ /dev/null @@ -1,11 +0,0 @@ -#version 330 core -out vec4 FragColor; - -in vec3 ourColor; -uniform vec4 ourColor2; // we set this variable in the OpenGL code. - -void main() -{ - //FragColor = vec4(ourColor, 1.0f); - FragColor = ourColor2; -} \ No newline at end of file diff --git a/samples/Basics/Texture/shader/textureShader.frag b/samples/Basics/Texture/shader/textureShader.frag deleted file mode 100644 index f0255cb..0000000 --- a/samples/Basics/Texture/shader/textureShader.frag +++ /dev/null @@ -1,16 +0,0 @@ -#version 330 core -out vec4 FragColor; - -in vec3 ourColor; -in vec2 TexCoord; - -uniform sampler2D texture1; -uniform sampler2D texture2; -uniform float mixValue; - -void main() -{ - // linearly interpolate between both textures (80% container, 20% awesomeface) - FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), texture(texture2, TexCoord).a * mixValue)*vec4(ourColor, 1.0); - //FragColor = mix(texture(texture1, TexCoord), texture(texture2, vec2(1.0 - TexCoord.x, TexCoord.y)), mixValue)*vec4(ourColor, 1.0); -} \ No newline at end of file diff --git a/samples/Basics/Texture/shader/textureShader.vert b/samples/Basics/Texture/shader/textureShader.vert deleted file mode 100644 index 311ec24..0000000 --- a/samples/Basics/Texture/shader/textureShader.vert +++ /dev/null @@ -1,14 +0,0 @@ -#version 330 core -layout (location = 0) in vec3 aPos; -layout (location = 1) in vec3 aColor; -layout (location = 2) in vec2 aTexCoord; - -out vec3 ourColor; -out vec2 TexCoord; - -void main() -{ - gl_Position = vec4(aPos, 1.0); - ourColor = aColor; - TexCoord = aTexCoord; -} \ No newline at end of file diff --git a/samples/Basics/Texture/src/main.cpp b/samples/Basics/Texture/src/main.cpp deleted file mode 100644 index 3e8be16..0000000 --- a/samples/Basics/Texture/src/main.cpp +++ /dev/null @@ -1,361 +0,0 @@ -#include -#include -#include "stb_image.h" - -#include "modules/shader_s.h" -#include "modules/filesystem.h" -#include "modules/window.h" - -#include - -#define START_FULLSCREEN 0 - -void framebuffer_size_callback(GLFWwindow* window, int width, int height); -void processInput(GLFWwindow *window); - -// settings -const unsigned int SCR_WIDTH = 800; -const unsigned int SCR_HEIGHT = 600; - -float mixValue = 0.0f; - -int main() -{ - // glfw: initialize and configure - // ------------------------------ - glfwInit(); - glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); - glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); - - #ifdef __APPLE__ - glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X - #endif - - // glfw window creation - // -------------------- - GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); - - if (window == NULL) - { - std::cout << "Failed to create GLFW window" << std::endl; - glfwTerminate(); - return -1; - } - glfwMakeContextCurrent(window); - glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); - - // glad: load all OpenGL function pointers - // --------------------------------------- - if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) - { - std::cout << "Failed to initialize GLAD" << std::endl; - return -1; - } - - icon(window); - -#if START_FULLSCREEN - // FULLSCREEN - const GLFWvidmode* mode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - glfwSetWindowMonitor(window, glfwGetPrimaryMonitor(), 0, 0, mode->width, mode->height, mode->refreshRate); -#endif - // build and compile our shader program - // ------------------------------------ - Shader ourShader(FileSystem::getSamplePath("shader/shader.vert").c_str(), FileSystem::getSamplePath("shader/shaderfs1.frag").c_str()); - Shader ourShader2(FileSystem::getSamplePath("shader/shader.vert").c_str(), FileSystem::getSamplePath("shader/shaderfs3.frag").c_str()); - Shader ourShader3(FileSystem::getSamplePath("shader/shader.vert").c_str(), FileSystem::getSamplePath("shader/shaderfs2.frag").c_str()); - Shader textureShader(FileSystem::getSamplePath("shader/textureShader.vert").c_str(), FileSystem::getSamplePath("shader/textureShader.frag").c_str()); - - // set up vertex data (and buffer(s)) and configure vertex attributes - // ------------------------------------------------------------------ - float firstTriangle[] = { - // positions // colors - -0.45f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // left - 0.45f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // right - 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f // top - }; - float secondTriangle[] = { - // positions // colors - 0.0f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // left - 0.9f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // right - 0.45f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top - }; - float vertices[] = { - // positions // colors // texture coords - 0.45f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right - 0.45f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right - -0.45f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left - -0.45f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left - }; - unsigned int indices[] = { // note that we start from 0! - 0, 1, 3, // first triangle - 1, 2, 3 // second triangle - }; - //Quader - float quader[] = { - // positions // colors // texture coords - 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right - 1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.f, 0.0f, // bottom right - -1.0f, -1.1f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left - -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left - }; - unsigned int quaderIndices[] = { - 0, 1, 3, // first triangle - 1, 2, 3 // second triangle - }; - - unsigned int VBOs[4], VAOs[4], EBOs[2]; - glGenVertexArrays(4, VAOs); // we can also generate multiple VAOs or buffers at the same time - glGenBuffers(4, VBOs); - glGenBuffers(2, EBOs); - - // first triangle setup - // -------------------- - glBindVertexArray(VAOs[0]); - glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]); - glBufferData(GL_ARRAY_BUFFER, sizeof(firstTriangle), firstTriangle, GL_STATIC_DRAW); - // position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - // color attribute - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - - // second triangle setup - // --------------------- - glBindVertexArray(VAOs[1]); // note that we bind to a different VAO now - glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]); // and a different VBO - glBufferData(GL_ARRAY_BUFFER, sizeof(secondTriangle), secondTriangle, GL_STATIC_DRAW); - // position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - // color attribute - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - - //EBO TRIANGLES - // 1. bind Vertex Array Object - glBindVertexArray(VAOs[2]); - // 2. copy our vertices array in a vertex buffer for OpenGL to use - glBindBuffer(GL_ARRAY_BUFFER, VBOs[2]); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - // 3. copy our index array in a element buffer for OpenGL to use - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBOs[0]); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); - // 4. then set the vertex attributes pointers - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - // texture coord attribute - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(2); - - //Quader - glBindVertexArray(VAOs[3]); - - glBindBuffer(GL_ARRAY_BUFFER, VBOs[3]); - glBufferData(GL_ARRAY_BUFFER, sizeof(quader), quader, GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBOs[1]); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(quaderIndices), quaderIndices, GL_STATIC_DRAW); - // position attribute - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); - glEnableVertexAttribArray(0); - // color attribute - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); - glEnableVertexAttribArray(1); - // texture coord attribute - glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); - glEnableVertexAttribArray(2); - - // load and create a texture - // ------------------------- - unsigned int texture1, texture2; - // texture 1 - //---------- - glGenTextures(1, &texture1); - glBindTexture(GL_TEXTURE_2D, texture1); - // set the texture wrapping/filtering options (on the currently bound texture object) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - // load image, create texture and generate mipmaps - stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture's on the y-axis. - // The FileSystem::getPath(...) is part of the GitHub repository so we can find files on any IDE/platform - int width, height, nrChannels; - unsigned char *data = stbi_load(FileSystem::getPath("content/images/container.jpg").c_str(), &width, &height, &nrChannels, 0); - - if (data) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); - //Arguments - // 1) specifies the texture target - // 2) mipmap level for which we want to create a texture for - // 3) what kind of format we want to store the texture - // 4&5) sets width & height of the resulting image - // 6) should always be 0 (legacy stzdd) - // 7&8) specify the format and datatype of the source image - // 9) actual image data - glGenerateMipmap(GL_TEXTURE_2D); - // automatically generate all the required mipmaps for the currently bound texture - } - else - { - std::cout << "Failed to load texture" << std::endl; - } - stbi_image_free(data); - // texture 2 - //---------- - glGenTextures(1, &texture2); - glBindTexture(GL_TEXTURE_2D, texture2); - // set the texture wrapping parameters - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method) - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); - // set texture filtering parameters - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - // load image, create texture and generate mipmaps - data = stbi_load(FileSystem::getPath("content/images/awesomeface.png").c_str(), &width, &height, &nrChannels, 0); - - if (data) { - // note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); - //Arguments - // 1) specifies the texture target - // 2) mipmap level for which we want to create a texture for - // 3) what kind of format we want to store the texture - // 4&5) sets width & height of the resulting image - // 6) should always be 0 (legacy stzdd) - // 7&8) specify the format and datatype of the source image - // 9) actual image data - glGenerateMipmap(GL_TEXTURE_2D); - // automatically generate all the required mipmaps for the currently bound texture - } - else - { - std::cout << "Failed to load texture" << std::endl; - } - stbi_image_free(data); - - - - // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind - glBindBuffer(GL_ARRAY_BUFFER, 0); - - // remember: do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound. - //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - - // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other - // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary. - glBindVertexArray(0); - - // uncomment this call to draw in wireframe polygons. - //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); - - // tell OpenGL for each sampler to which texture unit it belongs to (only has to be done once) - // ------------------------------------------------------------------------------------------- - textureShader.use(); // don't forget to activate/use the shader before setting uniforms! - // either set it manually like so: - glUniform1i(glGetUniformLocation(textureShader.ID, "texture1"), 0); - textureShader.setInt("texture2", 1); // or set it via the texture class - - - // render loop - // ----------- - while (!glfwWindowShouldClose(window)) - { - // input - // ----- - processInput(window); - - // render - // ------ - glClearColor(0.2f, 0.3f, 0.3f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - - - //THIRD Quader - ourShader3.use(); - glBindVertexArray(VAOs[2]); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - - // render the triangle - ourShader.use(); - //FIRST TRIANGLE - glBindVertexArray(VAOs[0]); - glDrawArrays(GL_TRIANGLES, 0, 3); // this call should output an orange triangle - - ourShader2.use(); - // update the uniform color - float timeValue = glfwGetTime(); - float greenValue = sin(timeValue) / 2.0f + 0.5f; - ourShader2.setFloat4("ourColor2", 0.0f, greenValue, 0.0f, 1.0f); - //SECOND TRIANGLE - glBindVertexArray(VAOs[1]); - glDrawArrays(GL_TRIANGLES, 0, 3); // this call should output a yellow triangle - - // bind Texture - // bind textures on corresponding texture units - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, texture1); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, texture2); - - // texturierte Sache Rendern - textureShader.use(); - // set the texture mix value in the shader - textureShader.setFloat("mixValue", mixValue); - glBindVertexArray(VAOs[3]); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - - // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) - // ------------------------------------------------------------------------------- - glfwSwapBuffers(window); - glfwPollEvents(); - } - - // optional: de-allocate all resources once they've outlived their purpose: - // ------------------------------------------------------------------------ - glDeleteVertexArrays(4, VAOs); - glDeleteBuffers(4, VBOs); - glDeleteBuffers(2, EBOs); - - // glfw: terminate, clearing all previously allocated GLFW resources. - // ------------------------------------------------------------------ - glfwTerminate(); - return 0; -} - -// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly -// --------------------------------------------------------------------------------------------------------- -void processInput(GLFWwindow *window) -{ - if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) - glfwSetWindowShouldClose(window, true); - - if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) - { - mixValue += 0.001f; - if (mixValue >= 1.0f) - mixValue = 1.0f; - } - - if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) - { - mixValue -= 0.001f; - if (mixValue <= 0.0f) - mixValue = 0.0f; - } -} - -// glfw: whenever the window size changed (by OS or user resize) this callback function executes -// --------------------------------------------------------------------------------------------- -void framebuffer_size_callback(GLFWwindow* window, int width, int height) -{ - // make sure the viewport matches the new window dimensions; note that width and - // height will be significantly larger than specified on retina displays. - glViewport(0, 0, width, height); -} \ No newline at end of file diff --git a/samples/Basics/Texture_Basics/README.md b/samples/Basics/Texture_Basics/README.md new file mode 100644 index 0000000..ff59f43 --- /dev/null +++ b/samples/Basics/Texture_Basics/README.md @@ -0,0 +1,13 @@ +# Texture basics + +This sample introduces 2D texture creation, sampler configuration, texture +units, mipmap generation, and blending two images in a fragment shader. + +| Key | Action | +| --- | --- | +| Up arrow | Increase the face texture contribution | +| Down arrow | Decrease the face texture contribution | +| `R` | Reset the blend to 20% | +| `Esc` | Quit | + +The window title displays the current blend percentage. diff --git a/samples/Basics/Texture_Basics/shader/texture.frag b/samples/Basics/Texture_Basics/shader/texture.frag new file mode 100644 index 0000000..167b6f7 --- /dev/null +++ b/samples/Basics/Texture_Basics/shader/texture.frag @@ -0,0 +1,14 @@ +#version 330 core + +out vec4 fragColor; +in vec2 texCoord; + +uniform sampler2D containerTexture; +uniform sampler2D faceTexture; +uniform float mixValue; + +void main() +{ + fragColor = mix(texture(containerTexture, texCoord), + texture(faceTexture, texCoord), mixValue); +} diff --git a/samples/Basics/Texture_Basics/shader/texture.vert b/samples/Basics/Texture_Basics/shader/texture.vert new file mode 100644 index 0000000..0719db4 --- /dev/null +++ b/samples/Basics/Texture_Basics/shader/texture.vert @@ -0,0 +1,12 @@ +#version 330 core + +layout (location = 0) in vec2 aPosition; +layout (location = 1) in vec2 aTexCoord; + +out vec2 texCoord; + +void main() +{ + gl_Position = vec4(aPosition, 0.0, 1.0); + texCoord = aTexCoord; +} diff --git a/samples/Basics/Texture_Basics/src/main.cpp b/samples/Basics/Texture_Basics/src/main.cpp new file mode 100644 index 0000000..6059dd2 --- /dev/null +++ b/samples/Basics/Texture_Basics/src/main.cpp @@ -0,0 +1,167 @@ +#include +#include +#include "stb_image.h" + +#include "modules/filesystem.h" +#include "modules/shader_s.h" +#include "modules/window.h" + +#include +#include +#include +#include + +namespace +{ +constexpr int WINDOW_WIDTH = 800; +constexpr int WINDOW_HEIGHT = 600; +float gMixValue = 0.2f; + +void framebufferSizeCallback(GLFWwindow*, int width, int height) +{ + glViewport(0, 0, width, height); +} + +void processInput(GLFWwindow* window, float deltaTime) +{ + if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) + glfwSetWindowShouldClose(window, true); + if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) gMixValue += deltaTime; + if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) gMixValue -= deltaTime; + if (glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS) gMixValue = 0.2f; + gMixValue = std::max(0.0f, std::min(1.0f, gMixValue)); +} + +GLuint loadTexture(const char* path, GLenum sourceFormat) +{ + int width = 0; + int height = 0; + int channels = 0; + unsigned char* pixels = stbi_load(path, &width, &height, &channels, 0); + if (!pixels) + { + std::cerr << "Failed to load texture: " << path << '\n'; + return 0; + } + + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexImage2D(GL_TEXTURE_2D, 0, sourceFormat, width, height, 0, + sourceFormat, GL_UNSIGNED_BYTE, pixels); + glGenerateMipmap(GL_TEXTURE_2D); + stbi_image_free(pixels); + return texture; +} +} + +int main() +{ + if (!glfwInit()) return -1; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + + GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Texture Basics", nullptr, nullptr); + if (!window) + { + glfwTerminate(); + return -1; + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); + if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) + { + glfwTerminate(); + return -1; + } + icon(window); + + const float vertices[] = { + // position // texture coordinate + -0.8f, -0.8f, 0.0f, 0.0f, + 0.8f, -0.8f, 1.0f, 0.0f, + 0.8f, 0.8f, 1.0f, 1.0f, + -0.8f, 0.8f, 0.0f, 1.0f + }; + const unsigned int indices[] = { 0, 1, 2, 0, 2, 3 }; + GLuint vao = 0, vbo = 0, ebo = 0; + glGenVertexArrays(1, &vao); + glGenBuffers(1, &vbo); + glGenBuffers(1, &ebo); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); + + stbi_set_flip_vertically_on_load(true); + const GLuint container = loadTexture(FileSystem::getPath("content/images/container.jpg").c_str(), GL_RGB); + const GLuint face = loadTexture(FileSystem::getPath("content/images/awesomeface.png").c_str(), GL_RGBA); + if (!container || !face) + { + glfwTerminate(); + return -1; + } + + Shader shader(FileSystem::getSamplePath("shader/texture.vert").c_str(), + FileSystem::getSamplePath("shader/texture.frag").c_str()); + shader.use(); + shader.setInt("containerTexture", 0); + shader.setInt("faceTexture", 1); + std::cout << "Texture Basics controls:\n" + << " Up/Down: blend textures R: reset Esc: quit\n"; + + double previousTime = glfwGetTime(); + double nextTitleUpdate = 0.0; + while (!glfwWindowShouldClose(window)) + { + const double time = glfwGetTime(); + const float deltaTime = static_cast(std::min(0.1, time - previousTime)); + previousTime = time; + processInput(window, deltaTime); + + glClearColor(0.08f, 0.10f, 0.13f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, container); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, face); + shader.use(); + shader.setFloat("mixValue", gMixValue); + glBindVertexArray(vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + + if (time >= nextTitleUpdate) + { + std::ostringstream title; + title << "Texture Basics | blend " << std::fixed << std::setprecision(0) + << gMixValue * 100.0f << "% | [Up/Down] blend [R] reset [Esc] quit"; + glfwSetWindowTitle(window, title.str().c_str()); + nextTitleUpdate = time + 0.2; + } + glfwSwapBuffers(window); + glfwPollEvents(); + } + + glDeleteTextures(1, &container); + glDeleteTextures(1, &face); + glDeleteVertexArrays(1, &vao); + glDeleteBuffers(1, &vbo); + glDeleteBuffers(1, &ebo); + glDeleteProgram(shader.ID); + glfwTerminate(); + return 0; +} diff --git a/samples/Basics/Texture_Compression/README.md b/samples/Basics/Texture_Compression/README.md new file mode 100644 index 0000000..3c6a9ca --- /dev/null +++ b/samples/Basics/Texture_Compression/README.md @@ -0,0 +1,21 @@ +# Texture compression + +This sample compares a generated RGBA8 image with DXT1/BC1 and ASTC 4x4 GPU +block-compressed representations. It implements +[issue #7](https://github.com/jonassorgenfrei/OpenGL/issues/7). + +| Key | Mode | +| --- | --- | +| `1` | RGBA8 source texture | +| `2` | DXT1 / BC1, encoded into 4x4 64-bit blocks | +| `3` | ASTC 4x4, encoded into 128-bit LDR void-extent blocks | +| `Esc` | Quit | + +DXT1 and ASTC are uploaded with `glCompressedTexImage2D`. If the active OpenGL +driver does not expose the corresponding extension, that mode uses the RGBA8 +texture as a safe fallback. The console distinguishes an unavailable extension +from a rejected upload and prints the active renderer; the window title marks +fallback modes. + +The compact encoders favor readable teaching code. Production applications +should normally use a mature offline encoder with higher-quality searches. diff --git a/samples/Basics/Texture_Compression/shader/compression.frag b/samples/Basics/Texture_Compression/shader/compression.frag new file mode 100644 index 0000000..7c5d0d4 --- /dev/null +++ b/samples/Basics/Texture_Compression/shader/compression.frag @@ -0,0 +1,11 @@ +#version 330 core + +out vec4 fragColor; +in vec2 texCoord; + +uniform sampler2D displayTexture; + +void main() +{ + fragColor = texture(displayTexture, texCoord); +} diff --git a/samples/Basics/Texture_Compression/shader/compression.vert b/samples/Basics/Texture_Compression/shader/compression.vert new file mode 100644 index 0000000..0719db4 --- /dev/null +++ b/samples/Basics/Texture_Compression/shader/compression.vert @@ -0,0 +1,12 @@ +#version 330 core + +layout (location = 0) in vec2 aPosition; +layout (location = 1) in vec2 aTexCoord; + +out vec2 texCoord; + +void main() +{ + gl_Position = vec4(aPosition, 0.0, 1.0); + texCoord = aTexCoord; +} diff --git a/samples/Basics/Texture_Compression/src/compression_encoders.h b/samples/Basics/Texture_Compression/src/compression_encoders.h new file mode 100644 index 0000000..1387b30 --- /dev/null +++ b/samples/Basics/Texture_Compression/src/compression_encoders.h @@ -0,0 +1,180 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace compression +{ +constexpr int BLOCK_SIZE = 4; + +inline std::uint8_t toByte(float value) +{ + value = std::max(0.0f, std::min(1.0f, value)); + return static_cast(value * 255.0f + 0.5f); +} + +// A grid, checker, gradient, and rings expose block-compression artifacts. +inline std::array testColor(int x, int y, int size) +{ + x = std::max(0, std::min(size - 1, x)); + y = std::max(0, std::min(size - 1, y)); + const float u = static_cast(x) / static_cast(size - 1); + const float v = static_cast(y) / static_cast(size - 1); + const float checker = (((x / (size / 16)) + (y / (size / 16))) & 1) ? 0.14f : 0.0f; + const float dx = u - 0.5f; + const float dy = v - 0.5f; + const float rings = 0.5f + 0.5f * std::sin(std::sqrt(dx * dx + dy * dy) * 95.0f); + const bool grid = x % (size / 8) < std::max(1, size / 256) || + y % (size / 8) < std::max(1, size / 256); + if (grid) return { 245, 245, 245, 255 }; + return { toByte(0.10f + 0.70f * u + checker), + toByte(0.10f + 0.70f * v + checker), + toByte(0.16f + 0.34f * rings + checker), 255 }; +} + +inline std::vector makeTestImage(int size) +{ + std::vector pixels(size * size * 4); + for (int y = 0; y < size; ++y) + for (int x = 0; x < size; ++x) + { + const auto color = testColor(x, y, size); + std::copy(color.begin(), color.end(), pixels.begin() + (y * size + x) * 4); + } + return pixels; +} + +inline std::uint16_t packRgb565(const std::uint8_t* color) +{ + return static_cast(((color[0] >> 3) << 11) | + ((color[1] >> 2) << 5) | + (color[2] >> 3)); +} + +inline std::array unpackRgb565(std::uint16_t color) +{ + return { static_cast(((color >> 11) & 31) * 255 / 31), + static_cast(((color >> 5) & 63) * 255 / 63), + static_cast((color & 31) * 255 / 31) }; +} + +// DXT1 stores two RGB565 endpoints and sixteen two-bit palette indices in each +// 64-bit block. This compact encoder uses the block's RGB bounding box. +inline std::vector encodeDxt1(const std::vector& rgba, + int width, int height) +{ + const int blocksX = (width + 3) / 4; + const int blocksY = (height + 3) / 4; + std::vector output(blocksX * blocksY * 8); + + for (int by = 0; by < blocksY; ++by) + { + for (int bx = 0; bx < blocksX; ++bx) + { + std::array minimum{ 255, 255, 255 }; + std::array maximum{ 0, 0, 0 }; + for (int py = 0; py < 4; ++py) + for (int px = 0; px < 4; ++px) + { + const int x = std::min(width - 1, bx * 4 + px); + const int y = std::min(height - 1, by * 4 + py); + const std::uint8_t* pixel = &rgba[(y * width + x) * 4]; + for (int channel = 0; channel < 3; ++channel) + { + minimum[channel] = std::min(minimum[channel], pixel[channel]); + maximum[channel] = std::max(maximum[channel], pixel[channel]); + } + } + + std::uint16_t color0 = packRgb565(maximum.data()); + std::uint16_t color1 = packRgb565(minimum.data()); + if (color0 <= color1) + { + if (color1 < 0xffff) color0 = static_cast(color1 + 1); + else color1 = static_cast(color0 - 1); + } + const auto c0 = unpackRgb565(color0); + const auto c1 = unpackRgb565(color1); + const std::array, 4> palette{ c0, c1, + std::array{ (2 * c0[0] + c1[0]) / 3, (2 * c0[1] + c1[1]) / 3, (2 * c0[2] + c1[2]) / 3 }, + std::array{ (c0[0] + 2 * c1[0]) / 3, (c0[1] + 2 * c1[1]) / 3, (c0[2] + 2 * c1[2]) / 3 } + }; + + std::uint32_t indices = 0; + for (int py = 0; py < 4; ++py) + for (int px = 0; px < 4; ++px) + { + const int x = std::min(width - 1, bx * 4 + px); + const int y = std::min(height - 1, by * 4 + py); + const std::uint8_t* pixel = &rgba[(y * width + x) * 4]; + int best = 0; + int bestError = std::numeric_limits::max(); + for (int candidate = 0; candidate < 4; ++candidate) + { + int error = 0; + for (int channel = 0; channel < 3; ++channel) + { + const int difference = static_cast(pixel[channel]) - palette[candidate][channel]; + error += difference * difference; + } + if (error < bestError) + { + best = candidate; + bestError = error; + } + } + indices |= static_cast(best) << (2 * (py * 4 + px)); + } + + std::uint8_t* block = &output[(by * blocksX + bx) * 8]; + block[0] = static_cast(color0); + block[1] = static_cast(color0 >> 8); + block[2] = static_cast(color1); + block[3] = static_cast(color1 >> 8); + for (int byte = 0; byte < 4; ++byte) + block[4 + byte] = static_cast(indices >> (byte * 8)); + } + } + return output; +} + +// A valid LDR void-extent block stores one averaged RGBA16 color. It is useful +// for demonstrating ASTC storage, while production encoders search endpoints +// and weights to retain detail within each block. +inline std::vector encodeAstc4x4(const std::vector& rgba, + int width, int height) +{ + const int blocksX = (width + BLOCK_SIZE - 1) / BLOCK_SIZE; + const int blocksY = (height + BLOCK_SIZE - 1) / BLOCK_SIZE; + std::vector output(blocksX * blocksY * 16, 0xff); + for (int by = 0; by < blocksY; ++by) + for (int bx = 0; bx < blocksX; ++bx) + { + std::array sum{}; + for (int py = 0; py < BLOCK_SIZE; ++py) + for (int px = 0; px < BLOCK_SIZE; ++px) + { + const int x = std::min(width - 1, bx * BLOCK_SIZE + px); + const int y = std::min(height - 1, by * BLOCK_SIZE + py); + const std::uint8_t* pixel = &rgba[(y * width + x) * 4]; + for (int channel = 0; channel < 4; ++channel) sum[channel] += pixel[channel]; + } + + std::uint8_t* block = &output[(by * blocksX + bx) * 16]; + block[0] = 0xfc; + block[1] = 0xfd; + for (int channel = 0; channel < 4; ++channel) + { + const std::uint16_t value8 = static_cast((sum[channel] + 8) / 16); + const std::uint16_t value16 = static_cast(value8 * 257); + block[8 + channel * 2] = static_cast(value16); + block[9 + channel * 2] = static_cast(value16 >> 8); + } + } + return output; +} +} diff --git a/samples/Basics/Texture_Compression/src/main.cpp b/samples/Basics/Texture_Compression/src/main.cpp new file mode 100644 index 0000000..74e18c5 --- /dev/null +++ b/samples/Basics/Texture_Compression/src/main.cpp @@ -0,0 +1,193 @@ +#include +#include + +#include "compression_encoders.h" +#include "modules/filesystem.h" +#include "modules/shader_s.h" +#include "modules/window.h" + +#include +#include +#include +#include +#include + +namespace +{ +constexpr int WINDOW_WIDTH = 900; +constexpr int WINDOW_HEIGHT = 700; +constexpr int IMAGE_SIZE = 256; +enum class Mode { Source, Dxt1, Astc }; + +Mode gMode = Mode::Source; +std::array gPreviousKeys{}; + +bool pressedOnce(GLFWwindow* window, int key) +{ + const bool pressed = glfwGetKey(window, key) == GLFW_PRESS; + const bool result = pressed && !gPreviousKeys[key]; + gPreviousKeys[key] = pressed; + return result; +} + +void processInput(GLFWwindow* window) +{ + if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) + glfwSetWindowShouldClose(window, true); + if (pressedOnce(window, GLFW_KEY_1)) gMode = Mode::Source; + if (pressedOnce(window, GLFW_KEY_2)) gMode = Mode::Dxt1; + if (pressedOnce(window, GLFW_KEY_3)) gMode = Mode::Astc; +} + +void framebufferSizeCallback(GLFWwindow*, int width, int height) +{ + glViewport(0, 0, width, height); +} + +GLuint createRgbaTexture(const std::vector& pixels) +{ + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, IMAGE_SIZE, IMAGE_SIZE, 0, + GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glGenerateMipmap(GL_TEXTURE_2D); + return texture; +} + +GLuint createCompressedTexture(GLenum format, const std::vector& blocks) +{ + GLuint texture = 0; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Extension presence alone is insufficient: confirm that storage exists + // after the driver processes the compressed upload. + while (glGetError() != GL_NO_ERROR) {} + glCompressedTexImage2D(GL_TEXTURE_2D, 0, format, IMAGE_SIZE, IMAGE_SIZE, 0, + static_cast(blocks.size()), blocks.data()); + GLint compressed = GL_FALSE; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compressed); + if (glGetError() == GL_NO_ERROR && compressed == GL_TRUE) return texture; + glDeleteTextures(1, &texture); + return 0; +} + +std::string modeName(bool dxtSupported, bool astcSupported) +{ + if (gMode == Mode::Source) return "RGBA8 source (256 KiB)"; + if (gMode == Mode::Dxt1) + return dxtSupported ? "DXT1 / BC1 (32 KiB, 8:1)" : "DXT1 unavailable - RGBA8 fallback"; + return astcSupported ? "ASTC 4x4 (64 KiB, 4:1)" : "ASTC unavailable - RGBA8 fallback"; +} +} + +int main() +{ + if (!glfwInit()) return -1; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Texture Compression", nullptr, nullptr); + if (!window) + { + glfwTerminate(); + return -1; + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); + if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) + { + glfwTerminate(); + return -1; + } + icon(window); + + const bool dxtExtension = GLAD_GL_EXT_texture_compression_s3tc != 0; + const bool astcExtension = GLAD_GL_KHR_texture_compression_astc_ldr != 0; + const auto sourcePixels = compression::makeTestImage(IMAGE_SIZE); + const auto dxtBlocks = compression::encodeDxt1(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); + const auto astcBlocks = compression::encodeAstc4x4(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); + const GLuint sourceTexture = createRgbaTexture(sourcePixels); + GLuint dxtTexture = dxtExtension ? createCompressedTexture(GL_COMPRESSED_RGB_S3TC_DXT1_EXT, dxtBlocks) : 0; + GLuint astcTexture = astcExtension ? createCompressedTexture(GL_COMPRESSED_RGBA_ASTC_4x4_KHR, astcBlocks) : 0; + const bool dxtSupported = dxtTexture != 0; + const bool astcSupported = astcTexture != 0; + if (!dxtTexture) dxtTexture = sourceTexture; + if (!astcTexture) astcTexture = sourceTexture; + + const char* dxtStatus = dxtSupported ? "yes" : + (dxtExtension ? "no (upload rejected; using RGBA8 fallback)" : "no (extension unavailable; using RGBA8 fallback)"); + const char* astcStatus = astcSupported ? "yes" : + (astcExtension ? "no (upload rejected; using RGBA8 fallback)" : "no (extension unavailable; using RGBA8 fallback)"); + std::cout << "Texture Compression controls:\n 1: RGBA8 2: DXT1/BC1 3: ASTC 4x4 Esc: quit\n\n" + << "OpenGL renderer: " << reinterpret_cast(glGetString(GL_RENDERER)) << '\n' + << "GPU DXT/S3TC support: " << dxtStatus << '\n' + << "GPU ASTC LDR support: " << astcStatus << '\n' + << "Payloads: RGBA8=" << sourcePixels.size() << ", DXT1=" << dxtBlocks.size() + << ", ASTC=" << astcBlocks.size() << " bytes\n"; + + const float vertices[] = { + -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f + }; + const unsigned int indices[] = { 0, 1, 2, 0, 2, 3 }; + GLuint vao = 0, vbo = 0, ebo = 0; + glGenVertexArrays(1, &vao); + glGenBuffers(1, &vbo); + glGenBuffers(1, &ebo); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); + + Shader shader(FileSystem::getSamplePath("shader/compression.vert").c_str(), + FileSystem::getSamplePath("shader/compression.frag").c_str()); + shader.use(); + shader.setInt("displayTexture", 0); + while (!glfwWindowShouldClose(window)) + { + processInput(window); + GLuint displayed = sourceTexture; + if (gMode == Mode::Dxt1) displayed = dxtTexture; + if (gMode == Mode::Astc) displayed = astcTexture; + glClear(GL_COLOR_BUFFER_BIT); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, displayed); + shader.use(); + glBindVertexArray(vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + const std::string title = "Texture Compression | " + modeName(dxtSupported, astcSupported) + + " | [1-3] mode [Esc] quit"; + glfwSetWindowTitle(window, title.c_str()); + glfwSwapBuffers(window); + glfwPollEvents(); + } + + if (dxtSupported) glDeleteTextures(1, &dxtTexture); + if (astcSupported) glDeleteTextures(1, &astcTexture); + glDeleteTextures(1, &sourceTexture); + glDeleteVertexArrays(1, &vao); + glDeleteBuffers(1, &vbo); + glDeleteBuffers(1, &ebo); + glDeleteProgram(shader.ID); + glfwTerminate(); + return 0; +} diff --git a/samples/Basics/Texture_VirtualTexturing/README.md b/samples/Basics/Texture_VirtualTexturing/README.md new file mode 100644 index 0000000..eac78ae --- /dev/null +++ b/samples/Basics/Texture_VirtualTexturing/README.md @@ -0,0 +1,22 @@ +# Virtual texturing + +This sample demonstrates a 2048x2048 logical texture backed by a bounded 11x11 +physical page cache. It implements +[issue #42](https://github.com/jonassorgenfrei/OpenGL/issues/42). + +| Key | Action | +| --- | --- | +| `WASD` / arrow keys | Pan | +| Mouse wheel / `Q` / `E` | Zoom | +| `R` | Reset the view | +| `Esc` | Quit | + +The implementation uses a stacked mip page table, LRU replacement, four uploads +per frame, and one-pixel filtering gutters. It chooses the finest visible mip +that fits the streaming cache and permanently keeps the 2x2 and 1x1 mip levels +resident. Missing detailed pages therefore use a complete coarse image rather +than exposing holes or thrashing while zoomed out. + +The logical RGBA8 image would occupy 16 MiB. Its physical cache and page table +use about 2.1 MiB, independent of the logical texture size. The title reports +cache occupancy, upload activity, active mip, and zoom level. diff --git a/samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.frag b/samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.frag new file mode 100644 index 0000000..a4309ee --- /dev/null +++ b/samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.frag @@ -0,0 +1,75 @@ +#version 330 core + +out vec4 fragColor; +in vec2 texCoord; + +uniform sampler2D physicalCache; +uniform sampler2D pageTable; +uniform vec2 viewCenter; +uniform float viewSpan; +uniform int activeMip; + +const float pageSize = 64.0; +const float slotSize = 66.0; +const float cacheSize = 726.0; + +int pageDimension(int mip) { return 32 >> mip; } + +int pageTableOffset(int mip) +{ + // Vertical layout: 32 + 16 + 8 + 4 + 2 + 1 page rows. + if (mip == 0) return 0; + if (mip == 1) return 32; + if (mip == 2) return 48; + if (mip == 3) return 56; + if (mip == 4) return 60; + return 62; +} + +void main() +{ + vec2 virtualUv = viewCenter + (texCoord - 0.5) * viewSpan; + vec4 entry = vec4(0.0); + int sampledMip = activeMip; + + // Walk toward the permanently resident coarse levels until a mapping is + // found. Fine pages replace this fallback as the bounded uploader runs. + for (int mip = 0; mip < 6; ++mip) + { + if (mip < activeMip) continue; + int dimension = pageDimension(mip); + ivec2 page = clamp(ivec2(floor(virtualUv * float(dimension))), + ivec2(0), ivec2(dimension - 1)); + entry = texelFetch(pageTable, + ivec2(page.x, page.y + pageTableOffset(mip)), 0); + sampledMip = mip; + if (entry.b >= 0.5) break; + } + + float sampledPages = float(pageDimension(sampledMip)); + vec2 pagePosition = virtualUv * sampledPages; + if (entry.b < 0.5) + { + // Defensive diagnostic: pinned fallback pages make this unreachable + // during normal operation. + vec2 cell = floor(pagePosition * 4.0); + float checker = mod(cell.x + cell.y, 2.0); + fragColor = vec4(mix(vec3(0.035, 0.04, 0.055), + vec3(0.14, 0.045, 0.13), checker), 1.0); + return; + } + + vec2 slot = floor(entry.rg * 255.0 + 0.5); + vec2 localUv = fract(pagePosition); + vec2 cachePixel = slot * slotSize + vec2(1.5) + localUv * (pageSize - 1.0); + vec2 cacheUv = cachePixel / cacheSize; + vec2 gradientX = dFdx(virtualUv) * (sampledPages * pageSize / cacheSize); + vec2 gradientY = dFdy(virtualUv) * (sampledPages * pageSize / cacheSize); + vec3 color = textureGrad(physicalCache, cacheUv, gradientX, gradientY).rgb; + + // The subtle border exposes page granularity for teaching and debugging. + float edge = min(min(localUv.x, localUv.y), + min(1.0 - localUv.x, 1.0 - localUv.y)); + float border = 1.0 - smoothstep(0.0, 0.018, edge); + fragColor = vec4(mix(color, vec3(0.02), border * 0.4), 1.0); +} diff --git a/samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.vert b/samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.vert new file mode 100644 index 0000000..0719db4 --- /dev/null +++ b/samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.vert @@ -0,0 +1,12 @@ +#version 330 core + +layout (location = 0) in vec2 aPosition; +layout (location = 1) in vec2 aTexCoord; + +out vec2 texCoord; + +void main() +{ + gl_Position = vec4(aPosition, 0.0, 1.0); + texCoord = aTexCoord; +} diff --git a/samples/Basics/Texture_VirtualTexturing/src/main.cpp b/samples/Basics/Texture_VirtualTexturing/src/main.cpp new file mode 100644 index 0000000..ee5dc08 --- /dev/null +++ b/samples/Basics/Texture_VirtualTexturing/src/main.cpp @@ -0,0 +1,180 @@ +#include +#include + +#include "modules/filesystem.h" +#include "modules/shader_s.h" +#include "modules/window.h" +#include "virtual_texture.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +constexpr int WINDOW_WIDTH = 1000; +constexpr int WINDOW_HEIGHT = 700; +float gCenterX = 0.5f; +float gCenterY = 0.5f; +float gViewSpan = 0.2f; +double gPendingScroll = 0.0; +std::array gPreviousKeys{}; + +float clampFloat(float value, float minimum, float maximum) +{ + return std::max(minimum, std::min(maximum, value)); +} + +bool pressedOnce(GLFWwindow* window, int key) +{ + const bool pressed = glfwGetKey(window, key) == GLFW_PRESS; + const bool result = pressed && !gPreviousKeys[key]; + gPreviousKeys[key] = pressed; + return result; +} + +void processInput(GLFWwindow* window, float deltaTime) +{ + if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) + glfwSetWindowShouldClose(window, true); + if (pressedOnce(window, GLFW_KEY_R)) + { + gCenterX = gCenterY = 0.5f; + gViewSpan = 0.2f; + } + + const float movement = gViewSpan * deltaTime * 0.8f; + if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS) gCenterX -= movement; + if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS) gCenterX += movement; + if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) gCenterY -= movement; + if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) gCenterY += movement; + if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) gViewSpan *= std::pow(0.35f, deltaTime); + if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) gViewSpan *= std::pow(2.85f, deltaTime); + if (gPendingScroll != 0.0) + { + gViewSpan *= std::pow(0.82f, static_cast(gPendingScroll)); + gPendingScroll = 0.0; + } + gViewSpan = clampFloat(gViewSpan, 0.025f, 1.0f); + const float halfSpan = gViewSpan * 0.5f; + gCenterX = clampFloat(gCenterX, halfSpan, 1.0f - halfSpan); + gCenterY = clampFloat(gCenterY, halfSpan, 1.0f - halfSpan); +} + +void framebufferSizeCallback(GLFWwindow*, int width, int height) +{ + glViewport(0, 0, width, height); +} + +void scrollCallback(GLFWwindow*, double, double yOffset) +{ + gPendingScroll += yOffset; +} +} + +int main() +{ + if (!glfwInit()) return -1; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); +#ifdef __APPLE__ + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#endif + GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, + "Virtual Texturing", nullptr, nullptr); + if (!window) + { + glfwTerminate(); + return -1; + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); + glfwSetScrollCallback(window, scrollCallback); + if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) + { + glfwTerminate(); + return -1; + } + icon(window); + + const float vertices[] = { + -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f + }; + const unsigned int indices[] = { 0, 1, 2, 0, 2, 3 }; + GLuint vao = 0, vbo = 0, ebo = 0; + glGenVertexArrays(1, &vao); + glGenBuffers(1, &vbo); + glGenBuffers(1, &ebo); + glBindVertexArray(vao); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), reinterpret_cast(2 * sizeof(float))); + glEnableVertexAttribArray(1); + + VirtualTexture virtualTexture; + virtualTexture.initialize(); + Shader shader(FileSystem::getSamplePath("shader/virtual_texture.vert").c_str(), + FileSystem::getSamplePath("shader/virtual_texture.frag").c_str()); + shader.use(); + shader.setInt("physicalCache", 0); + shader.setInt("pageTable", 1); + std::cout << "Virtual Texturing controls:\n" + << " WASD/arrows: pan mouse wheel or Q/E: zoom R: reset Esc: quit\n"; + + double previousTime = glfwGetTime(); + double nextTitleUpdate = 0.0; + while (!glfwWindowShouldClose(window)) + { + const double time = glfwGetTime(); + const float deltaTime = static_cast(std::min(0.1, time - previousTime)); + previousTime = time; + processInput(window, deltaTime); + virtualTexture.update(gCenterX, gCenterY, gViewSpan); + + glClearColor(0.025f, 0.03f, 0.045f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + shader.use(); + glUniform2f(glGetUniformLocation(shader.ID, "viewCenter"), gCenterX, gCenterY); + shader.setFloat("viewSpan", gViewSpan); + shader.setInt("activeMip", virtualTexture.activeMip()); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, virtualTexture.cacheTexture()); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTableTexture()); + glBindVertexArray(vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + + if (time >= nextTitleUpdate) + { + std::ostringstream title; + title << "Virtual Texturing | cache " << virtualTexture.residentCount() + << "/" << virtualTexture.slotCount() + << ", uploads " << virtualTexture.uploadsThisFrame() << "/frame" + << ", mip " << virtualTexture.activeMip() + << ", view " << std::fixed << std::setprecision(1) << gViewSpan * 100.0f + << "% | [WASD/arrows] pan [wheel/Q/E] zoom [R] reset [Esc] quit"; + glfwSetWindowTitle(window, title.str().c_str()); + nextTitleUpdate = time + 0.2; + } + glfwSwapBuffers(window); + glfwPollEvents(); + } + + virtualTexture.destroy(); + glDeleteVertexArrays(1, &vao); + glDeleteBuffers(1, &vbo); + glDeleteBuffers(1, &ebo); + glDeleteProgram(shader.ID); + glfwTerminate(); + return 0; +} diff --git a/samples/Basics/Texture_VirtualTexturing/src/virtual_texture.cpp b/samples/Basics/Texture_VirtualTexturing/src/virtual_texture.cpp new file mode 100644 index 0000000..85db768 --- /dev/null +++ b/samples/Basics/Texture_VirtualTexturing/src/virtual_texture.cpp @@ -0,0 +1,249 @@ +#include "virtual_texture.h" + +#include +#include +#include +#include + +namespace +{ +constexpr int VirtualSize = 2048; +constexpr int PageSize = 64; +constexpr int VirtualPages = VirtualSize / PageSize; +constexpr int MipLevels = 6; +constexpr int PinnedMip = 4; +constexpr int PageTableHeight = 63; +constexpr int PageGutter = 1; +constexpr int UploadsPerFrame = 4; + +std::uint8_t toByte(float value) +{ + value = std::max(0.0f, std::min(1.0f, value)); + return static_cast(value * 255.0f + 0.5f); +} + +// Procedural texels keep the residency example independent of disk I/O. +std::array virtualColor(int x, int y) +{ + x = std::max(0, std::min(VirtualSize - 1, x)); + y = std::max(0, std::min(VirtualSize - 1, y)); + const float u = static_cast(x) / static_cast(VirtualSize - 1); + const float v = static_cast(y) / static_cast(VirtualSize - 1); + const float checker = (((x / 128) + (y / 128)) & 1) ? 0.14f : 0.0f; + const float dx = u - 0.5f; + const float dy = v - 0.5f; + const float rings = 0.5f + 0.5f * std::sin(std::sqrt(dx * dx + dy * dy) * 95.0f); + const bool grid = x % 256 < 8 || y % 256 < 8; + if (grid) return { 245, 245, 245, 255 }; + return { toByte(0.10f + 0.70f * u + checker), + toByte(0.10f + 0.70f * v + checker), + toByte(0.16f + 0.34f * rings + checker), 255 }; +} +} + +int VirtualTexture::mipDimension(int mip) +{ + return std::max(1, VirtualPages >> mip); +} + +int VirtualTexture::tableYOffset(int mip) +{ + int offset = 0; + for (int level = 0; level < mip; ++level) offset += mipDimension(level); + return offset; +} + +int VirtualTexture::mipPageOffset(int mip) +{ + int offset = 0; + for (int level = 0; level < mip; ++level) + { + const int dimension = mipDimension(level); + offset += dimension * dimension; + } + return offset; +} + +int VirtualTexture::pageIndex(int mip, int x, int y) +{ + return mipPageOffset(mip) + y * mipDimension(mip) + x; +} + +VirtualTexture::PageRect VirtualTexture::requestedRect(int mip, float centerX, + float centerY, float viewSpan) +{ + const int dimension = mipDimension(mip); + const float halfSpan = viewSpan * 0.5f; + return { + std::max(0, static_cast(std::floor((centerX - halfSpan) * dimension)) - 1), + std::min(dimension - 1, static_cast(std::floor((centerX + halfSpan) * dimension)) + 1), + std::max(0, static_cast(std::floor((centerY - halfSpan) * dimension)) - 1), + std::min(dimension - 1, static_cast(std::floor((centerY + halfSpan) * dimension)) + 1) + }; +} + +std::size_t VirtualTexture::tablePixelOffset(const Page& page) const +{ + return static_cast(((tableYOffset(page.mip) + page.y) * + VirtualPages + page.x) * 4); +} + +void VirtualTexture::initialize() +{ + tablePixels_.assign(VirtualPages * PageTableHeight * 4, 0); + for (int mip = 0; mip < MipLevels; ++mip) + { + const int dimension = mipDimension(mip); + for (int y = 0; y < dimension; ++y) + for (int x = 0; x < dimension; ++x) + pages_.push_back({ -1, mip, x, y }); + } + + glGenTextures(1, &cache_); + glBindTexture(GL_TEXTURE_2D, cache_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, CacheSize, CacheSize, 0, + GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + glGenTextures(1, &pageTable_); + glBindTexture(GL_TEXTURE_2D, pageTable_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, VirtualPages, PageTableHeight, 0, + GL_RGBA, GL_UNSIGNED_BYTE, tablePixels_.data()); +} + +void VirtualTexture::uploadPage(int requestedIndex) +{ + int selectedSlot = -1; + std::uint64_t oldest = std::numeric_limits::max(); + for (int index = 0; index < static_cast(slots_.size()); ++index) + { + if (slots_[index].page < 0) + { + selectedSlot = index; + break; + } + if (pages_[slots_[index].page].mip >= PinnedMip) continue; + if (slots_[index].lastUsed < oldest) + { + oldest = slots_[index].lastUsed; + selectedSlot = index; + } + } + if (selectedSlot < 0) return; + + Slot& slot = slots_[selectedSlot]; + if (slot.page >= 0) + { + Page& evicted = pages_[slot.page]; + evicted.slot = -1; + std::fill_n(&tablePixels_[tablePixelOffset(evicted)], 4, 0); + } + else + { + ++residentCount_; + } + + Page& requested = pages_[requestedIndex]; + const int mipScale = 1 << requested.mip; + std::vector pixels(SlotSize * SlotSize * 4); + for (int y = 0; y < SlotSize; ++y) + for (int x = 0; x < SlotSize; ++x) + { + const int virtualX = (requested.x * PageSize + x - PageGutter) * mipScale + mipScale / 2; + const int virtualY = (requested.y * PageSize + y - PageGutter) * mipScale + mipScale / 2; + const auto color = virtualColor(virtualX, virtualY); + std::copy(color.begin(), color.end(), pixels.begin() + (y * SlotSize + x) * 4); + } + + const int slotX = selectedSlot % CachePages; + const int slotY = selectedSlot / CachePages; + glBindTexture(GL_TEXTURE_2D, cache_); + glTexSubImage2D(GL_TEXTURE_2D, 0, slotX * SlotSize, slotY * SlotSize, + SlotSize, SlotSize, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + + slot.page = requestedIndex; + slot.lastUsed = frame_; + requested.slot = selectedSlot; + std::uint8_t* entry = &tablePixels_[tablePixelOffset(requested)]; + entry[0] = static_cast(slotX); + entry[1] = static_cast(slotY); + entry[2] = entry[3] = 255; +} + +void VirtualTexture::update(float centerX, float centerY, float viewSpan) +{ + ++frame_; + uploadsThisFrame_ = 0; + + // These five pages guarantee a complete fallback image from frame one. + if (frame_ == 1) + for (int mip = PinnedMip; mip < MipLevels; ++mip) + { + const int dimension = mipDimension(mip); + for (int y = 0; y < dimension; ++y) + for (int x = 0; x < dimension; ++x) + uploadPage(pageIndex(mip, x, y)); + } + + constexpr int pinnedPages = 5; // 2x2 plus 1x1 + const int streamingCapacity = slotCount() - pinnedPages; + activeMip_ = PinnedMip - 1; + PageRect rectangle = requestedRect(activeMip_, centerX, centerY, viewSpan); + for (int mip = 0; mip < PinnedMip; ++mip) + { + const PageRect candidate = requestedRect(mip, centerX, centerY, viewSpan); + if (candidate.count() <= streamingCapacity) + { + activeMip_ = mip; + rectangle = candidate; + break; + } + } + + std::vector requested; + requested.reserve(static_cast(rectangle.count())); + for (int y = rectangle.minY; y <= rectangle.maxY; ++y) + for (int x = rectangle.minX; x <= rectangle.maxX; ++x) + requested.push_back(pageIndex(activeMip_, x, y)); + + const int dimension = mipDimension(activeMip_); + std::sort(requested.begin(), requested.end(), [&](int a, int b) + { + const Page& pageA = pages_[a]; + const Page& pageB = pages_[b]; + const float ax = (pageA.x + 0.5f) / dimension - centerX; + const float ay = (pageA.y + 0.5f) / dimension - centerY; + const float bx = (pageB.x + 0.5f) / dimension - centerX; + const float by = (pageB.y + 0.5f) / dimension - centerY; + return ax * ax + ay * ay < bx * bx + by * by; + }); + + // Protect the entire visible set before the LRU pass selects victims. + for (int index : requested) + if (pages_[index].slot >= 0) + slots_[pages_[index].slot].lastUsed = frame_; + for (int index : requested) + if (pages_[index].slot < 0 && uploadsThisFrame_ < UploadsPerFrame) + { + uploadPage(index); + ++uploadsThisFrame_; + } + + glBindTexture(GL_TEXTURE_2D, pageTable_); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VirtualPages, PageTableHeight, + GL_RGBA, GL_UNSIGNED_BYTE, tablePixels_.data()); +} + +void VirtualTexture::destroy() +{ + glDeleteTextures(1, &cache_); + glDeleteTextures(1, &pageTable_); + cache_ = pageTable_ = 0; +} diff --git a/samples/Basics/Texture_VirtualTexturing/src/virtual_texture.h b/samples/Basics/Texture_VirtualTexturing/src/virtual_texture.h new file mode 100644 index 0000000..8ace628 --- /dev/null +++ b/samples/Basics/Texture_VirtualTexturing/src/virtual_texture.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +#include +#include +#include +#include + +class VirtualTexture +{ +public: + static constexpr int CachePages = 11; + static constexpr int SlotSize = 66; + static constexpr int CacheSize = CachePages * SlotSize; + + void initialize(); + void update(float centerX, float centerY, float viewSpan); + void destroy(); + + GLuint cacheTexture() const { return cache_; } + GLuint pageTableTexture() const { return pageTable_; } + int residentCount() const { return residentCount_; } + int slotCount() const { return static_cast(slots_.size()); } + int uploadsThisFrame() const { return uploadsThisFrame_; } + int activeMip() const { return activeMip_; } + +private: + struct Page + { + int slot = -1; + int mip = 0; + int x = 0; + int y = 0; + }; + + struct Slot + { + int page = -1; + std::uint64_t lastUsed = 0; + }; + + struct PageRect + { + int minX = 0; + int maxX = 0; + int minY = 0; + int maxY = 0; + int count() const { return (maxX - minX + 1) * (maxY - minY + 1); } + }; + + static int mipDimension(int mip); + static int tableYOffset(int mip); + static int mipPageOffset(int mip); + static int pageIndex(int mip, int x, int y); + static PageRect requestedRect(int mip, float centerX, float centerY, float viewSpan); + std::size_t tablePixelOffset(const Page& page) const; + void uploadPage(int pageIndex); + + GLuint cache_ = 0; + GLuint pageTable_ = 0; + std::vector pages_; + std::array slots_{}; + std::vector tablePixels_; + std::uint64_t frame_ = 0; + int residentCount_ = 0; + int uploadsThisFrame_ = 0; + int activeMip_ = 0; +};