From 42081718d225ed8a8b7e83c61a00eb6c3f90fb1c Mon Sep 17 00:00:00 2001 From: Jonas Sorgenfrei Date: Sat, 12 Sep 2026 20:40:08 +0200 Subject: [PATCH 1/4] Add compressed and virtual texture lab --- samples/Basics/Texture/README.md | 27 + .../Basics/Texture/shader/textureShader.frag | 57 +- .../Basics/Texture/shader/textureShader.vert | 16 +- samples/Basics/Texture/src/main.cpp | 937 +++++++++++------- 4 files changed, 675 insertions(+), 362 deletions(-) create mode 100644 samples/Basics/Texture/README.md diff --git a/samples/Basics/Texture/README.md b/samples/Basics/Texture/README.md new file mode 100644 index 0000000..6dfb7bd --- /dev/null +++ b/samples/Basics/Texture/README.md @@ -0,0 +1,27 @@ +# Texture lab + +This sample compares a generated RGBA8 source image with two GPU block-compressed +representations and a software-managed virtual texture. +It starts in virtual-texture mode so page streaming is visible immediately. +It implements [ASTC + DXT Texturing issue #7](https://github.com/jonassorgenfrei/OpenGL/issues/7) +and [Virtual Texturing issue #42](https://github.com/jonassorgenfrei/OpenGL/issues/42). + +| 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 | +| `4` | 2048x2048 virtual texture backed by a 10x10 physical page cache | + +In virtual-texture mode, use `WASD` or the arrow keys to pan, the mouse wheel or +`Q`/`E` to zoom, and `R` to reset the view. Missing pages are purple while the +cache incrementally fills. The window title reports residency and upload activity. + +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 and reports this in both the console and window title. + +The virtual texture demonstrates page-table indirection, bounded physical +storage, LRU replacement, an upload budget, and one-pixel page gutters. Its +logical image would require 16 MiB as RGBA8; the physical cache and page table +use about 1.7 MiB regardless of logical texture size. diff --git a/samples/Basics/Texture/shader/textureShader.frag b/samples/Basics/Texture/shader/textureShader.frag index f0255cb..126bef7 100644 --- a/samples/Basics/Texture/shader/textureShader.frag +++ b/samples/Basics/Texture/shader/textureShader.frag @@ -1,16 +1,51 @@ #version 330 core -out vec4 FragColor; - -in vec3 ourColor; -in vec2 TexCoord; -uniform sampler2D texture1; -uniform sampler2D texture2; -uniform float mixValue; +out vec4 fragColor; +in vec2 texCoord; + +uniform sampler2D displayTexture; +uniform sampler2D physicalCache; +uniform sampler2D pageTable; +uniform bool virtualMode; +uniform vec2 viewCenter; +uniform float viewSpan; + +const float virtualPages = 32.0; +const float pageSize = 64.0; +const float slotSize = 66.0; +const float cacheSize = 660.0; 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 + if (!virtualMode) + { + fragColor = texture(displayTexture, texCoord); + return; + } + + vec2 virtualUv = viewCenter + (texCoord - 0.5) * viewSpan; + vec2 pagePosition = virtualUv * virtualPages; + ivec2 page = clamp(ivec2(floor(pagePosition)), ivec2(0), ivec2(31)); + vec4 entry = texelFetch(pageTable, page, 0); + vec2 gradientX = dFdx(virtualUv) * (virtualPages * pageSize / cacheSize); + vec2 gradientY = dFdy(virtualUv) * (virtualPages * pageSize / cacheSize); + + if (entry.b < 0.5) + { + vec2 checkerCell = floor(pagePosition * 4.0); + float checker = mod(checkerCell.x + checkerCell.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; + vec3 color = textureGrad(physicalCache, cacheUv, gradientX, gradientY).rgb; + + float edgeDistance = min(min(localUv.x, localUv.y), min(1.0 - localUv.x, 1.0 - localUv.y)); + float pageBorder = 1.0 - smoothstep(0.0, 0.018, edgeDistance); + color = mix(color, vec3(0.02), pageBorder * 0.4); + fragColor = vec4(color, 1.0); +} diff --git a/samples/Basics/Texture/shader/textureShader.vert b/samples/Basics/Texture/shader/textureShader.vert index 311ec24..0719db4 100644 --- a/samples/Basics/Texture/shader/textureShader.vert +++ b/samples/Basics/Texture/shader/textureShader.vert @@ -1,14 +1,12 @@ #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; +layout (location = 0) in vec2 aPosition; +layout (location = 1) in vec2 aTexCoord; + +out vec2 texCoord; void main() { - gl_Position = vec4(aPos, 1.0); - ourColor = aColor; - TexCoord = aTexCoord; -} \ No newline at end of file + gl_Position = vec4(aPosition, 0.0, 1.0); + texCoord = aTexCoord; +} diff --git a/samples/Basics/Texture/src/main.cpp b/samples/Basics/Texture/src/main.cpp index 3e8be16..a983c13 100644 --- a/samples/Basics/Texture/src/main.cpp +++ b/samples/Basics/Texture/src/main.cpp @@ -1,361 +1,614 @@ #include #include -#include "stb_image.h" -#include "modules/shader_s.h" #include "modules/filesystem.h" +#include "modules/shader_s.h" #include "modules/window.h" +#include +#include +#include +#include +#include #include +#include +#include +#include +#include -#define START_FULLSCREEN 0 +namespace +{ +constexpr int WINDOW_WIDTH = 1000; +constexpr int WINDOW_HEIGHT = 700; +constexpr int IMAGE_SIZE = 256; +constexpr int ASTC_BLOCK = 4; +constexpr int VIRTUAL_SIZE = 2048; +constexpr int PAGE_SIZE = 64; +constexpr int VIRTUAL_PAGES = VIRTUAL_SIZE / PAGE_SIZE; +constexpr int CACHE_PAGES = 10; +constexpr int PAGE_GUTTER = 1; +constexpr int SLOT_SIZE = PAGE_SIZE + PAGE_GUTTER * 2; +constexpr int CACHE_SIZE = CACHE_PAGES * SLOT_SIZE; +constexpr int UPLOADS_PER_FRAME = 4; + +enum class Mode { Source, Dxt1, Astc, Virtual }; + +struct VirtualTexture +{ + struct Page { int slot = -1; }; + struct Slot { int page = -1; std::uint64_t lastUsed = 0; }; + + GLuint cache = 0; + GLuint pageTable = 0; + std::array pages{}; + std::array slots{}; + std::vector tablePixels; + std::uint64_t frame = 0; + int residentCount = 0; + int uploadsThisFrame = 0; +}; + +Mode gMode = Mode::Virtual; +float gCenterX = 0.5f; +float gCenterY = 0.5f; +float gViewSpan = 0.20f; +double gPendingScroll = 0.0; +std::array gPreviousKeys{}; + +float clampFloat(float value, float minimum, float maximum) +{ + return std::max(minimum, std::min(maximum, value)); +} -void framebuffer_size_callback(GLFWwindow* window, int width, int height); -void processInput(GLFWwindow *window); +std::uint8_t toByte(float value) +{ + return static_cast(clampFloat(value, 0.0f, 1.0f) * 255.0f + 0.5f); +} -// settings -const unsigned int SCR_WIDTH = 800; -const unsigned int SCR_HEIGHT = 600; +std::array proceduralColor(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 int cellX = x / std::max(1, size / 16); + const int cellY = y / std::max(1, size / 16); + const float checker = ((cellX + cellY) & 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 int gridStep = std::max(1, size / 8); + const int gridWidth = std::max(1, size / 256); + const bool majorGrid = x % gridStep < gridWidth || y % gridStep < gridWidth; + + float r = 0.10f + 0.70f * u + checker; + float g = 0.10f + 0.70f * v + checker; + float b = 0.16f + 0.34f * rings + checker; + if (majorGrid) r = g = b = 0.96f; + return { toByte(r), toByte(g), toByte(b), 255 }; +} -float mixValue = 0.0f; +std::vector makeSourceImage() +{ + std::vector pixels(IMAGE_SIZE * IMAGE_SIZE * 4); + for (int y = 0; y < IMAGE_SIZE; ++y) + { + for (int x = 0; x < IMAGE_SIZE; ++x) + { + const auto color = proceduralColor(x, y, IMAGE_SIZE); + std::copy(color.begin(), color.end(), pixels.begin() + (y * IMAGE_SIZE + x) * 4); + } + } + return pixels; +} -int main() +std::uint16_t packRgb565(const std::uint8_t* color) { - // 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; + return static_cast(((color[0] >> 3) << 11) | + ((color[1] >> 2) << 5) | + (color[2] >> 3)); +} + +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) + }; +} + +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); + 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) + { + bestError = error; + best = candidate; + } + } + 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 compact educational ASTC encoder using valid LDR void-extent blocks. +std::vector encodeAstc4x4(const std::vector& rgba, int width, int height) +{ + const int blocksX = (width + ASTC_BLOCK - 1) / ASTC_BLOCK; + const int blocksY = (height + ASTC_BLOCK - 1) / ASTC_BLOCK; + 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 < ASTC_BLOCK; ++py) + { + for (int px = 0; px < ASTC_BLOCK; ++px) + { + const int x = std::min(width - 1, bx * ASTC_BLOCK + px); + const int y = std::min(height - 1, by * ASTC_BLOCK + 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; // bits 8..0: ASTC void-extent marker + block[1] = 0xfd; // LDR, reserved bits and all extent bits set + 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; +} + +GLuint createRgbaTexture(const std::vector& pixels, int width, int height) +{ + 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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + glGenerateMipmap(GL_TEXTURE_2D); + return texture; +} + +GLuint createCompressedTexture(GLenum format, const std::vector& blocks, int width, int height) +{ + 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); + while (glGetError() != GL_NO_ERROR) {} + glCompressedTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, + static_cast(blocks.size()), blocks.data()); + GLint storedCompressed = GL_FALSE; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &storedCompressed); + if (glGetError() != GL_NO_ERROR || storedCompressed != GL_TRUE) + { + glDeleteTextures(1, &texture); + return 0; + } + return texture; } -// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly -// --------------------------------------------------------------------------------------------------------- -void processInput(GLFWwindow *window) +void initializeVirtualTexture(VirtualTexture& virtualTexture) { - 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; - } + virtualTexture.tablePixels.assign(VIRTUAL_PAGES * VIRTUAL_PAGES * 4, 0); + + glGenTextures(1, &virtualTexture.cache); + glBindTexture(GL_TEXTURE_2D, virtualTexture.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, CACHE_SIZE, CACHE_SIZE, 0, GL_RGBA, + GL_UNSIGNED_BYTE, nullptr); + + glGenTextures(1, &virtualTexture.pageTable); + glBindTexture(GL_TEXTURE_2D, virtualTexture.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, VIRTUAL_PAGES, VIRTUAL_PAGES, 0, + GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); } -// 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) +void uploadPage(VirtualTexture& virtualTexture, int pageIndex) { - // 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 + int selectedSlot = -1; + std::uint64_t oldest = std::numeric_limits::max(); + for (int slotIndex = 0; slotIndex < static_cast(virtualTexture.slots.size()); ++slotIndex) + { + if (virtualTexture.slots[slotIndex].page < 0) + { + selectedSlot = slotIndex; + break; + } + if (virtualTexture.slots[slotIndex].lastUsed < oldest) + { + oldest = virtualTexture.slots[slotIndex].lastUsed; + selectedSlot = slotIndex; + } + } + + VirtualTexture::Slot& slot = virtualTexture.slots[selectedSlot]; + if (slot.page >= 0) + { + virtualTexture.pages[slot.page].slot = -1; + std::fill_n(&virtualTexture.tablePixels[slot.page * 4], 4, 0); + } + else + { + ++virtualTexture.residentCount; + } + + const int pageX = pageIndex % VIRTUAL_PAGES; + const int pageY = pageIndex / VIRTUAL_PAGES; + std::vector pixels(SLOT_SIZE * SLOT_SIZE * 4); + for (int y = 0; y < SLOT_SIZE; ++y) + { + for (int x = 0; x < SLOT_SIZE; ++x) + { + const int virtualX = pageX * PAGE_SIZE + x - PAGE_GUTTER; + const int virtualY = pageY * PAGE_SIZE + y - PAGE_GUTTER; + const auto color = proceduralColor(virtualX, virtualY, VIRTUAL_SIZE); + std::copy(color.begin(), color.end(), pixels.begin() + (y * SLOT_SIZE + x) * 4); + } + } + + const int slotX = selectedSlot % CACHE_PAGES; + const int slotY = selectedSlot / CACHE_PAGES; + glBindTexture(GL_TEXTURE_2D, virtualTexture.cache); + glTexSubImage2D(GL_TEXTURE_2D, 0, slotX * SLOT_SIZE, slotY * SLOT_SIZE, + SLOT_SIZE, SLOT_SIZE, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + + slot.page = pageIndex; + slot.lastUsed = virtualTexture.frame; + virtualTexture.pages[pageIndex].slot = selectedSlot; + std::uint8_t* entry = &virtualTexture.tablePixels[pageIndex * 4]; + entry[0] = static_cast(slotX); + entry[1] = static_cast(slotY); + entry[2] = 255; + entry[3] = 255; +} + +void updateVirtualTexture(VirtualTexture& virtualTexture) +{ + ++virtualTexture.frame; + virtualTexture.uploadsThisFrame = 0; + const float halfSpan = gViewSpan * 0.5f; + const int minX = std::max(0, static_cast(std::floor((gCenterX - halfSpan) * VIRTUAL_PAGES)) - 1); + const int maxX = std::min(VIRTUAL_PAGES - 1, static_cast(std::floor((gCenterX + halfSpan) * VIRTUAL_PAGES)) + 1); + const int minY = std::max(0, static_cast(std::floor((gCenterY - halfSpan) * VIRTUAL_PAGES)) - 1); + const int maxY = std::min(VIRTUAL_PAGES - 1, static_cast(std::floor((gCenterY + halfSpan) * VIRTUAL_PAGES)) + 1); + + std::vector requested; + for (int y = minY; y <= maxY; ++y) + for (int x = minX; x <= maxX; ++x) + requested.push_back(y * VIRTUAL_PAGES + x); + + std::sort(requested.begin(), requested.end(), [](int a, int b) + { + const float ax = (a % VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterX; + const float ay = (a / VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterY; + const float bx = (b % VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterX; + const float by = (b / VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterY; + return ax * ax + ay * ay < bx * bx + by * by; + }); + + for (int pageIndex : requested) + { + VirtualTexture::Page& page = virtualTexture.pages[pageIndex]; + if (page.slot >= 0) + { + virtualTexture.slots[page.slot].lastUsed = virtualTexture.frame; + } + else if (virtualTexture.uploadsThisFrame < UPLOADS_PER_FRAME) + { + uploadPage(virtualTexture, pageIndex); + ++virtualTexture.uploadsThisFrame; + } + } + + glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTable); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIRTUAL_PAGES, VIRTUAL_PAGES, + GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); +} + +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_1)) gMode = Mode::Source; + if (pressedOnce(window, GLFW_KEY_2)) gMode = Mode::Dxt1; + if (pressedOnce(window, GLFW_KEY_3)) gMode = Mode::Astc; + if (pressedOnce(window, GLFW_KEY_4)) gMode = Mode::Virtual; + if (pressedOnce(window, GLFW_KEY_R)) + { + gCenterX = gCenterY = 0.5f; + gViewSpan = 0.20f; + } + + 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); +} + +std::string modeName(Mode mode, bool dxtSupported, bool astcSupported) +{ + switch (mode) + { + case Mode::Source: return "RGBA8 source (256 x 256, 256 KiB)"; + case Mode::Dxt1: return dxtSupported ? "DXT1 / BC1 (32 KiB, 8:1)" : "DXT1 unavailable - RGBA8 fallback"; + case Mode::Astc: return astcSupported ? "ASTC 4x4 (64 KiB, 4:1)" : "ASTC unavailable - RGBA8 fallback"; + case Mode::Virtual: return "Virtual texture (2048 x 2048 logical)"; + } + return {}; +} + +void updateTitle(GLFWwindow* window, const VirtualTexture& virtualTexture, + bool dxtSupported, bool astcSupported) +{ + std::ostringstream title; + title << "Texture Lab | " << modeName(gMode, dxtSupported, astcSupported); + if (gMode == Mode::Virtual) + { + title << " | cache " << virtualTexture.residentCount << "/" << virtualTexture.slots.size() + << ", uploads " << virtualTexture.uploadsThisFrame << "/frame" + << ", view " << std::fixed << std::setprecision(1) << gViewSpan * 100.0f << "%"; + } + title << " | [1-4] mode [WASD/arrows] pan [wheel/Q/E] zoom [R] reset [Esc] quit"; + glfwSetWindowTitle(window, title.str().c_str()); +} + +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, "Texture Lab", nullptr, nullptr); + if (!window) + { + std::cerr << "Failed to create GLFW window\n"; + glfwTerminate(); + return -1; + } + glfwMakeContextCurrent(window); + glfwSwapInterval(1); + glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); + glfwSetScrollCallback(window, scrollCallback); + + if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) + { + std::cerr << "Failed to initialize GLAD\n"; + 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 std::vector sourcePixels = makeSourceImage(); + const std::vector dxtBlocks = encodeDxt1(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); + const std::vector astcBlocks = encodeAstc4x4(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); + const GLuint sourceTexture = createRgbaTexture(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); + GLuint dxtTexture = dxtExtension + ? createCompressedTexture(GL_COMPRESSED_RGB_S3TC_DXT1_EXT, dxtBlocks, IMAGE_SIZE, IMAGE_SIZE) + : 0; + GLuint astcTexture = astcExtension + ? createCompressedTexture(GL_COMPRESSED_RGBA_ASTC_4x4_KHR, astcBlocks, IMAGE_SIZE, IMAGE_SIZE) + : 0; + const bool dxtSupported = dxtTexture != 0; + const bool astcSupported = astcTexture != 0; + if (!dxtTexture) dxtTexture = sourceTexture; + if (!astcTexture) astcTexture = sourceTexture; + + std::cout << "Texture Lab controls:\n" + << " 1: RGBA8 source 2: DXT1/BC1 3: ASTC 4x4 4: virtual texture\n" + << " WASD/arrows: pan mouse wheel or Q/E: zoom R: reset Esc: quit\n\n" + << "GPU DXT/S3TC support: " << (dxtSupported ? "yes" : "no (using RGBA8 fallback)") << '\n' + << "GPU ASTC LDR support: " << (astcSupported ? "yes" : "no (using RGBA8 fallback)") << '\n' + << "Generated payloads: RGBA8=" << sourcePixels.size() << " bytes, DXT1=" << dxtBlocks.size() + << " bytes, ASTC=" << astcBlocks.size() << " bytes\n"; + + VirtualTexture virtualTexture; + initializeVirtualTexture(virtualTexture); + + 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/textureShader.vert").c_str(), + FileSystem::getSamplePath("shader/textureShader.frag").c_str()); + shader.use(); + shader.setInt("displayTexture", 0); + shader.setInt("physicalCache", 1); + shader.setInt("pageTable", 2); + + 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); + if (gMode == Mode::Virtual) updateVirtualTexture(virtualTexture); + + glClearColor(0.025f, 0.03f, 0.045f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + shader.use(); + shader.setInt("virtualMode", gMode == Mode::Virtual ? 1 : 0); + glUniform2f(glGetUniformLocation(shader.ID, "viewCenter"), gCenterX, gCenterY); + shader.setFloat("viewSpan", gViewSpan); + + glActiveTexture(GL_TEXTURE0); + GLuint displayTexture = sourceTexture; + if (gMode == Mode::Dxt1) displayTexture = dxtTexture; + if (gMode == Mode::Astc) displayTexture = astcTexture; + glBindTexture(GL_TEXTURE_2D, displayTexture); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, virtualTexture.cache); + glActiveTexture(GL_TEXTURE2); + glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTable); + glBindVertexArray(vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); + + if (time >= nextTitleUpdate) + { + updateTitle(window, virtualTexture, dxtSupported, astcSupported); + nextTitleUpdate = time + 0.2; + } + glfwSwapBuffers(window); + glfwPollEvents(); + } + + if (dxtSupported) glDeleteTextures(1, &dxtTexture); + if (astcSupported) glDeleteTextures(1, &astcTexture); + glDeleteTextures(1, &sourceTexture); + glDeleteTextures(1, &virtualTexture.cache); + glDeleteTextures(1, &virtualTexture.pageTable); + glDeleteVertexArrays(1, &vao); + glDeleteBuffers(1, &vbo); + glDeleteBuffers(1, &ebo); + glDeleteProgram(shader.ID); + glfwTerminate(); + return 0; +} From 409b992c087e2cd33db5ec7bfe385480461eb184 Mon Sep 17 00:00:00 2001 From: Jonas Sorgenfrei Date: Sat, 12 Sep 2026 21:15:50 +0200 Subject: [PATCH 2/4] docs(texture): explain compression and page streaming --- .../Basics/Texture/shader/textureShader.frag | 7 +++ samples/Basics/Texture/src/main.cpp | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/samples/Basics/Texture/shader/textureShader.frag b/samples/Basics/Texture/shader/textureShader.frag index 126bef7..bd3be1b 100644 --- a/samples/Basics/Texture/shader/textureShader.frag +++ b/samples/Basics/Texture/shader/textureShader.frag @@ -23,13 +23,17 @@ void main() return; } + // Resolve the screen coordinate into logical texture and page space. vec2 virtualUv = viewCenter + (texCoord - 0.5) * viewSpan; vec2 pagePosition = virtualUv * virtualPages; ivec2 page = clamp(ivec2(floor(pagePosition)), ivec2(0), ivec2(31)); vec4 entry = texelFetch(pageTable, page, 0); + // Logical-space derivatives keep filtering stable across discontinuous + // physical cache addresses. vec2 gradientX = dFdx(virtualUv) * (virtualPages * pageSize / cacheSize); vec2 gradientY = dFdy(virtualUv) * (virtualPages * pageSize / cacheSize); + // Show missing residency while the bounded uploader fills the cache. if (entry.b < 0.5) { vec2 checkerCell = floor(pagePosition * 4.0); @@ -38,12 +42,15 @@ void main() return; } + // Remap the texel into its physical slot. Sampling inside the one-texel + // gutter prevents linear filtering from leaking neighboring cache slots. 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; vec3 color = textureGrad(physicalCache, cacheUv, gradientX, gradientY).rgb; + // A subtle border makes page granularity visible for teaching and debugging. float edgeDistance = min(min(localUv.x, localUv.y), min(1.0 - localUv.x, 1.0 - localUv.y)); float pageBorder = 1.0 - smoothstep(0.0, 0.018, edgeDistance); color = mix(color, vec3(0.02), pageBorder * 0.4); diff --git a/samples/Basics/Texture/src/main.cpp b/samples/Basics/Texture/src/main.cpp index a983c13..1f8888e 100644 --- a/samples/Basics/Texture/src/main.cpp +++ b/samples/Basics/Texture/src/main.cpp @@ -18,6 +18,9 @@ namespace { +// The source image is deliberately small enough to compare formats directly. +// The virtual image is larger, but only CACHE_PAGES squared pages ever reside +// in GPU memory. Each physical page includes a one-texel filtering gutter. constexpr int WINDOW_WIDTH = 1000; constexpr int WINDOW_HEIGHT = 700; constexpr int IMAGE_SIZE = 256; @@ -35,7 +38,11 @@ enum class Mode { Source, Dxt1, Astc, Virtual }; struct VirtualTexture { + // Logical pages point into the physical cache. A value of -1 means that + // the shader renders the missing-page diagnostic until the page is streamed. struct Page { int slot = -1; }; + + // Cache slots track ownership and age for least-recently-used replacement. struct Slot { int page = -1; std::uint64_t lastUsed = 0; }; GLuint cache = 0; @@ -65,6 +72,8 @@ std::uint8_t toByte(float value) return static_cast(clampFloat(value, 0.0f, 1.0f) * 255.0f + 0.5f); } +// Gradients, a grid, a checker, and rings expose compression artifacts and +// virtual page boundaries more clearly than a smooth source image would. std::array proceduralColor(int x, int y, int size) { x = std::max(0, std::min(size - 1, x)); @@ -102,6 +111,8 @@ std::vector makeSourceImage() return pixels; } +// DXT1 stores two RGB565 endpoints and sixteen two-bit palette indices per +// 4x4 block. This teaching encoder favors clarity over exhaustive endpoint search. std::uint16_t packRgb565(const std::uint8_t* color) { return static_cast(((color[0] >> 3) << 11) | @@ -128,6 +139,7 @@ std::vector encodeDxt1(const std::vector& rgba, int { for (int bx = 0; bx < blocksX; ++bx) { + // Select a bounding-box endpoint pair for this 4x4 footprint. std::array minimum{ 255, 255, 255 }; std::array maximum{ 0, 0, 0 }; for (int py = 0; py < 4; ++py) @@ -153,6 +165,7 @@ std::vector encodeDxt1(const std::vector& rgba, int else color1 = static_cast(color0 - 1); } + // Four-color mode derives two palette entries between the endpoints. const auto c0 = unpackRgb565(color0); const auto c1 = unpackRgb565(color1); std::array, 4> palette{ c0, c1, @@ -160,6 +173,7 @@ std::vector encodeDxt1(const std::vector& rgba, int std::array{ (c0[0] + 2 * c1[0]) / 3, (c0[1] + 2 * c1[1]) / 3, (c0[2] + 2 * c1[2]) / 3 } }; + // Assign each texel to its least-squares palette match. std::uint32_t indices = 0; for (int py = 0; py < 4; ++py) { @@ -201,6 +215,9 @@ std::vector encodeDxt1(const std::vector& rgba, int } // A compact educational ASTC encoder using valid LDR void-extent blocks. +// ASTC stores 128 bits per block; this representation averages each 4x4 +// footprint into one RGBA16 constant. Production endpoint/weight searches +// belong in an offline compressor such as astcenc. std::vector encodeAstc4x4(const std::vector& rgba, int width, int height) { const int blocksX = (width + ASTC_BLOCK - 1) / ASTC_BLOCK; @@ -223,6 +240,8 @@ std::vector encodeAstc4x4(const std::vector& rgba, i } } + // Bits 10..63 are one, leaving the void extent unspecified. + // Bytes 8..15 contain little-endian RGBA UNORM16 values. std::uint8_t* block = &output[(by * blocksX + bx) * 16]; block[0] = 0xfc; // bits 8..0: ASTC void-extent marker block[1] = 0xfd; // LDR, reserved bits and all extent bits set @@ -261,6 +280,8 @@ GLuint createCompressedTexture(GLenum format, const std::vector& b 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 is only the first check. The upload and the texture's + // reported storage state confirm that the driver accepted the format. while (glGetError() != GL_NO_ERROR) {} glCompressedTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, static_cast(blocks.size()), blocks.data()); @@ -276,6 +297,8 @@ GLuint createCompressedTexture(GLenum format, const std::vector& b void initializeVirtualTexture(VirtualTexture& virtualTexture) { + // The physical cache owns texels. The smaller RGBA8 page table maps each + // logical page to cache-slot XY and carries a residency flag in blue. virtualTexture.tablePixels.assign(VIRTUAL_PAGES * VIRTUAL_PAGES * 4, 0); glGenTextures(1, &virtualTexture.cache); @@ -299,6 +322,8 @@ void initializeVirtualTexture(VirtualTexture& virtualTexture) void uploadPage(VirtualTexture& virtualTexture, int pageIndex) { + // Prefer a free slot; once full, replace the globally least-recently-used + // slot and invalidate the evicted page's table entry. int selectedSlot = -1; std::uint64_t oldest = std::numeric_limits::max(); for (int slotIndex = 0; slotIndex < static_cast(virtualTexture.slots.size()); ++slotIndex) @@ -326,6 +351,8 @@ void uploadPage(VirtualTexture& virtualTexture, int pageIndex) ++virtualTexture.residentCount; } + // Generate the requested page and its border texels on demand. A real + // streamer would usually obtain this payload from disk or a worker thread. const int pageX = pageIndex % VIRTUAL_PAGES; const int pageY = pageIndex / VIRTUAL_PAGES; std::vector pixels(SLOT_SIZE * SLOT_SIZE * 4); @@ -340,12 +367,14 @@ void uploadPage(VirtualTexture& virtualTexture, int pageIndex) } } + // Update only one atlas slot instead of reallocating the physical cache. const int slotX = selectedSlot % CACHE_PAGES; const int slotY = selectedSlot / CACHE_PAGES; glBindTexture(GL_TEXTURE_2D, virtualTexture.cache); glTexSubImage2D(GL_TEXTURE_2D, 0, slotX * SLOT_SIZE, slotY * SLOT_SIZE, SLOT_SIZE, SLOT_SIZE, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); + // Publish the new mapping after its texels have reached the cache. slot.page = pageIndex; slot.lastUsed = virtualTexture.frame; virtualTexture.pages[pageIndex].slot = selectedSlot; @@ -366,11 +395,13 @@ void updateVirtualTexture(VirtualTexture& virtualTexture) const int minY = std::max(0, static_cast(std::floor((gCenterY - halfSpan) * VIRTUAL_PAGES)) - 1); const int maxY = std::min(VIRTUAL_PAGES - 1, static_cast(std::floor((gCenterY + halfSpan) * VIRTUAL_PAGES)) + 1); + // Request the visible rectangle plus a one-page prefetch border. std::vector requested; for (int y = minY; y <= maxY; ++y) for (int x = minX; x <= maxX; ++x) requested.push_back(y * VIRTUAL_PAGES + x); + // Stream center-first so the most noticeable holes fill first. std::sort(requested.begin(), requested.end(), [](int a, int b) { const float ax = (a % VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterX; @@ -380,6 +411,8 @@ void updateVirtualTexture(VirtualTexture& virtualTexture) return ax * ax + ay * ay < bx * bx + by * by; }); + // Touch resident pages for LRU accounting and cap misses to avoid a camera + // jump causing an unbounded upload hitch in one frame. for (int pageIndex : requested) { VirtualTexture::Page& page = virtualTexture.pages[pageIndex]; @@ -394,6 +427,7 @@ void updateVirtualTexture(VirtualTexture& virtualTexture) } } + // At 32x32 texels the complete page table is cheap to upload each frame. glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTable); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIRTUAL_PAGES, VIRTUAL_PAGES, GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); @@ -401,6 +435,7 @@ void updateVirtualTexture(VirtualTexture& virtualTexture) bool pressedOnce(GLFWwindow* window, int key) { + // Edge detection prevents a held key from repeatedly changing modes. const bool pressed = glfwGetKey(window, key) == GLFW_PRESS; const bool result = pressed && !gPreviousKeys[key]; gPreviousKeys[key] = pressed; @@ -478,6 +513,8 @@ void scrollCallback(GLFWwindow*, double, double yOffset) int main() { + // Compressed formats are optional even though the sample uses a portable + // OpenGL 3.3 context, so all compressed allocations are capability-checked. if (!glfwInit()) return -1; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); @@ -506,6 +543,8 @@ int main() } icon(window); + // Encode the same source into every comparison format. A rejected upload + // safely aliases the original RGBA8 texture as its fallback. const bool dxtExtension = GLAD_GL_EXT_texture_compression_s3tc != 0; const bool astcExtension = GLAD_GL_KHR_texture_compression_astc_ldr != 0; const std::vector sourcePixels = makeSourceImage(); @@ -531,9 +570,12 @@ int main() << "Generated payloads: RGBA8=" << sourcePixels.size() << " bytes, DXT1=" << dxtBlocks.size() << " bytes, ASTC=" << astcBlocks.size() << " bytes\n"; + // This software-managed indirection does not require a hardware sparse- + // texture extension, which keeps the residency algorithm visible and portable. VirtualTexture virtualTexture; initializeVirtualTexture(virtualTexture); + // A full-screen quad compares the sampling paths without scene distractions. const float vertices[] = { -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, @@ -564,6 +606,8 @@ int main() double previousTime = glfwGetTime(); double nextTitleUpdate = 0.0; + // Residency is updated before drawing so a new mapping is visible in the + // same frame as its cache upload. while (!glfwWindowShouldClose(window)) { const double time = glfwGetTime(); @@ -600,6 +644,8 @@ int main() glfwPollEvents(); } + // Fallback textures alias sourceTexture; only successful compressed + // allocations own a separate object and may be deleted independently. if (dxtSupported) glDeleteTextures(1, &dxtTexture); if (astcSupported) glDeleteTextures(1, &astcTexture); glDeleteTextures(1, &sourceTexture); From 4ef87f351676a14057a51bcc1f2394b5ad3c6b47 Mon Sep 17 00:00:00 2001 From: Jonas Sorgenfrei Date: Sun, 13 Sep 2026 17:12:10 +0200 Subject: [PATCH 3/4] fix(texture): add mip fallback for virtual residency --- samples/Basics/Texture/README.md | 19 +- .../Basics/Texture/shader/textureShader.frag | 48 +++- samples/Basics/Texture/src/main.cpp | 209 ++++++++++++++---- 3 files changed, 221 insertions(+), 55 deletions(-) diff --git a/samples/Basics/Texture/README.md b/samples/Basics/Texture/README.md index 6dfb7bd..c7127fe 100644 --- a/samples/Basics/Texture/README.md +++ b/samples/Basics/Texture/README.md @@ -11,17 +11,22 @@ and [Virtual Texturing issue #42](https://github.com/jonassorgenfrei/OpenGL/issu | `1` | RGBA8 source texture | | `2` | DXT1 / BC1, encoded into 4x4 64-bit blocks | | `3` | ASTC 4x4, encoded into 128-bit LDR void-extent blocks | -| `4` | 2048x2048 virtual texture backed by a 10x10 physical page cache | +| `4` | 2048x2048 virtual texture backed by an 11x11 physical page cache | In virtual-texture mode, use `WASD` or the arrow keys to pan, the mouse wheel or -`Q`/`E` to zoom, and `R` to reset the view. Missing pages are purple while the -cache incrementally fills. The window title reports residency and upload activity. +`Q`/`E` to zoom, and `R` to reset the view. The window title reports residency, +upload activity, and the currently requested mip level. 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 and reports this in both the console and window title. +The console distinguishes an unavailable extension from a rejected compressed +upload and prints the active renderer to make driver capability issues explicit. -The virtual texture demonstrates page-table indirection, bounded physical -storage, LRU replacement, an upload budget, and one-pixel page gutters. Its -logical image would require 16 MiB as RGBA8; the physical cache and page table -use about 1.7 MiB regardless of logical texture size. +The virtual texture demonstrates a stacked mip page table, bounded physical +storage, LRU replacement, an upload budget, and one-pixel page gutters. It +selects the finest visible mip that fits the streaming cache and permanently +keeps the 2x2 and 1x1 mip levels resident. Missing detailed pages therefore +fall back to a complete coarse image instead of exposing cache holes or +thrashing when the view is zoomed out. The 16 MiB logical RGBA8 image uses +about 2.1 MiB of physical cache and page-table storage. diff --git a/samples/Basics/Texture/shader/textureShader.frag b/samples/Basics/Texture/shader/textureShader.frag index bd3be1b..cf0bb4d 100644 --- a/samples/Basics/Texture/shader/textureShader.frag +++ b/samples/Basics/Texture/shader/textureShader.frag @@ -9,11 +9,27 @@ uniform sampler2D pageTable; uniform bool virtualMode; uniform vec2 viewCenter; uniform float viewSpan; +uniform int activeMip; -const float virtualPages = 32.0; const float pageSize = 64.0; const float slotSize = 66.0; -const float cacheSize = 660.0; +const float cacheSize = 726.0; + +int pageDimension(int mip) +{ + return 32 >> mip; +} + +int pageTableOffset(int mip) +{ + // The mip page tables are packed vertically: 32 + 16 + 8 + 4 + 2 + 1. + 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() { @@ -23,17 +39,31 @@ void main() return; } - // Resolve the screen coordinate into logical texture and page space. + // Resolve the screen coordinate into logical texture space, then select + // the requested mip or the first coarser page that is already resident. vec2 virtualUv = viewCenter + (texCoord - 0.5) * viewSpan; - vec2 pagePosition = virtualUv * virtualPages; - ivec2 page = clamp(ivec2(floor(pagePosition)), ivec2(0), ivec2(31)); - vec4 entry = texelFetch(pageTable, page, 0); + vec4 entry = vec4(0.0); + int sampledMip = activeMip; + for (int mip = 0; mip < 6; ++mip) + { + if (mip < activeMip) continue; + int dimension = pageDimension(mip); + vec2 candidatePosition = virtualUv * float(dimension); + ivec2 page = clamp(ivec2(floor(candidatePosition)), 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; // Logical-space derivatives keep filtering stable across discontinuous // physical cache addresses. - vec2 gradientX = dFdx(virtualUv) * (virtualPages * pageSize / cacheSize); - vec2 gradientY = dFdy(virtualUv) * (virtualPages * pageSize / cacheSize); + vec2 gradientX = dFdx(virtualUv) * (sampledPages * pageSize / cacheSize); + vec2 gradientY = dFdy(virtualUv) * (sampledPages * pageSize / cacheSize); - // Show missing residency while the bounded uploader fills the cache. + // The two coarsest levels are pinned, so this is only a defensive marker + // for a broken page-table mapping rather than normal streaming behavior. if (entry.b < 0.5) { vec2 checkerCell = floor(pagePosition * 4.0); diff --git a/samples/Basics/Texture/src/main.cpp b/samples/Basics/Texture/src/main.cpp index 1f8888e..daa00f4 100644 --- a/samples/Basics/Texture/src/main.cpp +++ b/samples/Basics/Texture/src/main.cpp @@ -28,7 +28,10 @@ constexpr int ASTC_BLOCK = 4; constexpr int VIRTUAL_SIZE = 2048; constexpr int PAGE_SIZE = 64; constexpr int VIRTUAL_PAGES = VIRTUAL_SIZE / PAGE_SIZE; -constexpr int CACHE_PAGES = 10; +constexpr int VIRTUAL_MIP_LEVELS = 6; // 32, 16, 8, 4, 2, and 1 pages per axis +constexpr int PINNED_MIP = 4; // 2x2 and 1x1 fallbacks never leave memory +constexpr int PAGE_TABLE_HEIGHT = 63; // sum of all mip dimensions +constexpr int CACHE_PAGES = 11; constexpr int PAGE_GUTTER = 1; constexpr int SLOT_SIZE = PAGE_SIZE + PAGE_GUTTER * 2; constexpr int CACHE_SIZE = CACHE_PAGES * SLOT_SIZE; @@ -39,20 +42,37 @@ enum class Mode { Source, Dxt1, Astc, Virtual }; struct VirtualTexture { // Logical pages point into the physical cache. A value of -1 means that - // the shader renders the missing-page diagnostic until the page is streamed. - struct Page { int slot = -1; }; + // the shader falls back to a coarser resident mip until this page streams. + struct Page + { + int slot = -1; + int mip = 0; + int x = 0; + int y = 0; + }; // Cache slots track ownership and age for least-recently-used replacement. struct Slot { int page = -1; std::uint64_t lastUsed = 0; }; GLuint cache = 0; GLuint pageTable = 0; - std::array pages{}; + std::vector pages; std::array slots{}; std::vector tablePixels; std::uint64_t frame = 0; int residentCount = 0; int uploadsThisFrame = 0; + int activeMip = 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); } }; Mode gMode = Mode::Virtual; @@ -67,6 +87,52 @@ float clampFloat(float value, float minimum, float maximum) return std::max(minimum, std::min(maximum, value)); } +int mipPageDimension(int mip) +{ + return std::max(1, VIRTUAL_PAGES >> mip); +} + +int pageTableYOffset(int mip) +{ + int offset = 0; + for (int level = 0; level < mip; ++level) offset += mipPageDimension(level); + return offset; +} + +int mipPageOffset(int mip) +{ + int offset = 0; + for (int level = 0; level < mip; ++level) + { + const int dimension = mipPageDimension(level); + offset += dimension * dimension; + } + return offset; +} + +int pageIndex(int mip, int x, int y) +{ + return mipPageOffset(mip) + y * mipPageDimension(mip) + x; +} + +std::size_t pageTablePixelOffset(const VirtualTexture::Page& page) +{ + const int tableY = pageTableYOffset(page.mip) + page.y; + return static_cast((tableY * VIRTUAL_PAGES + page.x) * 4); +} + +PageRect requestedPageRect(int mip) +{ + const int dimension = mipPageDimension(mip); + const float halfSpan = gViewSpan * 0.5f; + return { + std::max(0, static_cast(std::floor((gCenterX - halfSpan) * dimension)) - 1), + std::min(dimension - 1, static_cast(std::floor((gCenterX + halfSpan) * dimension)) + 1), + std::max(0, static_cast(std::floor((gCenterY - halfSpan) * dimension)) - 1), + std::min(dimension - 1, static_cast(std::floor((gCenterY + halfSpan) * dimension)) + 1) + }; +} + std::uint8_t toByte(float value) { return static_cast(clampFloat(value, 0.0f, 1.0f) * 255.0f + 0.5f); @@ -297,9 +363,16 @@ GLuint createCompressedTexture(GLenum format, const std::vector& b void initializeVirtualTexture(VirtualTexture& virtualTexture) { - // The physical cache owns texels. The smaller RGBA8 page table maps each - // logical page to cache-slot XY and carries a residency flag in blue. - virtualTexture.tablePixels.assign(VIRTUAL_PAGES * VIRTUAL_PAGES * 4, 0); + // The physical cache owns texels. All mip page tables are stacked vertically + // in one RGBA8 texture; entries store cache-slot XY and residency in blue. + virtualTexture.tablePixels.assign(VIRTUAL_PAGES * PAGE_TABLE_HEIGHT * 4, 0); + for (int mip = 0; mip < VIRTUAL_MIP_LEVELS; ++mip) + { + const int dimension = mipPageDimension(mip); + for (int y = 0; y < dimension; ++y) + for (int x = 0; x < dimension; ++x) + virtualTexture.pages.push_back({ -1, mip, x, y }); + } glGenTextures(1, &virtualTexture.cache); glBindTexture(GL_TEXTURE_2D, virtualTexture.cache); @@ -316,7 +389,7 @@ void initializeVirtualTexture(VirtualTexture& virtualTexture) 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, VIRTUAL_PAGES, VIRTUAL_PAGES, 0, + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, VIRTUAL_PAGES, PAGE_TABLE_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); } @@ -333,18 +406,24 @@ void uploadPage(VirtualTexture& virtualTexture, int pageIndex) selectedSlot = slotIndex; break; } + if (virtualTexture.pages[virtualTexture.slots[slotIndex].page].mip >= PINNED_MIP) + continue; if (virtualTexture.slots[slotIndex].lastUsed < oldest) { oldest = virtualTexture.slots[slotIndex].lastUsed; selectedSlot = slotIndex; } } + // The caller limits streaming pages to the non-pinned capacity, so this + // guard can only trigger if those cache invariants are changed later. + if (selectedSlot < 0) return; VirtualTexture::Slot& slot = virtualTexture.slots[selectedSlot]; if (slot.page >= 0) { - virtualTexture.pages[slot.page].slot = -1; - std::fill_n(&virtualTexture.tablePixels[slot.page * 4], 4, 0); + VirtualTexture::Page& evictedPage = virtualTexture.pages[slot.page]; + evictedPage.slot = -1; + std::fill_n(&virtualTexture.tablePixels[pageTablePixelOffset(evictedPage)], 4, 0); } else { @@ -353,15 +432,17 @@ void uploadPage(VirtualTexture& virtualTexture, int pageIndex) // Generate the requested page and its border texels on demand. A real // streamer would usually obtain this payload from disk or a worker thread. - const int pageX = pageIndex % VIRTUAL_PAGES; - const int pageY = pageIndex / VIRTUAL_PAGES; + VirtualTexture::Page& requestedPage = virtualTexture.pages[pageIndex]; + const int mipScale = 1 << requestedPage.mip; std::vector pixels(SLOT_SIZE * SLOT_SIZE * 4); for (int y = 0; y < SLOT_SIZE; ++y) { for (int x = 0; x < SLOT_SIZE; ++x) { - const int virtualX = pageX * PAGE_SIZE + x - PAGE_GUTTER; - const int virtualY = pageY * PAGE_SIZE + y - PAGE_GUTTER; + // Sample the center of the source footprint represented by this + // mip texel. Gutter coordinates naturally fetch adjacent pages. + const int virtualX = (requestedPage.x * PAGE_SIZE + x - PAGE_GUTTER) * mipScale + mipScale / 2; + const int virtualY = (requestedPage.y * PAGE_SIZE + y - PAGE_GUTTER) * mipScale + mipScale / 2; const auto color = proceduralColor(virtualX, virtualY, VIRTUAL_SIZE); std::copy(color.begin(), color.end(), pixels.begin() + (y * SLOT_SIZE + x) * 4); } @@ -377,8 +458,8 @@ void uploadPage(VirtualTexture& virtualTexture, int pageIndex) // Publish the new mapping after its texels have reached the cache. slot.page = pageIndex; slot.lastUsed = virtualTexture.frame; - virtualTexture.pages[pageIndex].slot = selectedSlot; - std::uint8_t* entry = &virtualTexture.tablePixels[pageIndex * 4]; + requestedPage.slot = selectedSlot; + std::uint8_t* entry = &virtualTexture.tablePixels[pageTablePixelOffset(requestedPage)]; entry[0] = static_cast(slotX); entry[1] = static_cast(slotY); entry[2] = 255; @@ -389,47 +470,87 @@ void updateVirtualTexture(VirtualTexture& virtualTexture) { ++virtualTexture.frame; virtualTexture.uploadsThisFrame = 0; - const float halfSpan = gViewSpan * 0.5f; - const int minX = std::max(0, static_cast(std::floor((gCenterX - halfSpan) * VIRTUAL_PAGES)) - 1); - const int maxX = std::min(VIRTUAL_PAGES - 1, static_cast(std::floor((gCenterX + halfSpan) * VIRTUAL_PAGES)) + 1); - const int minY = std::max(0, static_cast(std::floor((gCenterY - halfSpan) * VIRTUAL_PAGES)) - 1); - const int maxY = std::min(VIRTUAL_PAGES - 1, static_cast(std::floor((gCenterY + halfSpan) * VIRTUAL_PAGES)) + 1); - // Request the visible rectangle plus a one-page prefetch border. + // Keep the 2x2 and 1x1 mip levels permanently resident. Until a detailed + // page arrives, the shader walks up to one of these complete fallbacks. + if (virtualTexture.frame == 1) + { + for (int mip = PINNED_MIP; mip < VIRTUAL_MIP_LEVELS; ++mip) + { + const int dimension = mipPageDimension(mip); + for (int y = 0; y < dimension; ++y) + for (int x = 0; x < dimension; ++x) + uploadPage(virtualTexture, pageIndex(mip, x, y)); + } + } + + int pinnedPageCount = 0; + for (int mip = PINNED_MIP; mip < VIRTUAL_MIP_LEVELS; ++mip) + { + const int dimension = mipPageDimension(mip); + pinnedPageCount += dimension * dimension; + } + const int streamingCapacity = static_cast(virtualTexture.slots.size()) - pinnedPageCount; + + // Choose the finest level whose visible rectangle and prefetch border fit + // in the streaming portion of the cache. This prevents visible-page churn + // when zooming out instead of blindly requesting hundreds of mip-0 pages. + virtualTexture.activeMip = PINNED_MIP - 1; + PageRect requestedRect = requestedPageRect(virtualTexture.activeMip); + for (int mip = 0; mip < PINNED_MIP; ++mip) + { + const PageRect candidate = requestedPageRect(mip); + if (candidate.count() <= streamingCapacity) + { + virtualTexture.activeMip = mip; + requestedRect = candidate; + break; + } + } + + const int activeDimension = mipPageDimension(virtualTexture.activeMip); std::vector requested; - for (int y = minY; y <= maxY; ++y) - for (int x = minX; x <= maxX; ++x) - requested.push_back(y * VIRTUAL_PAGES + x); + requested.reserve(static_cast(requestedRect.count())); + for (int y = requestedRect.minY; y <= requestedRect.maxY; ++y) + for (int x = requestedRect.minX; x <= requestedRect.maxX; ++x) + requested.push_back(pageIndex(virtualTexture.activeMip, x, y)); // Stream center-first so the most noticeable holes fill first. - std::sort(requested.begin(), requested.end(), [](int a, int b) + std::sort(requested.begin(), requested.end(), [&](int a, int b) { - const float ax = (a % VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterX; - const float ay = (a / VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterY; - const float bx = (b % VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterX; - const float by = (b / VIRTUAL_PAGES + 0.5f) / VIRTUAL_PAGES - gCenterY; + const VirtualTexture::Page& pageA = virtualTexture.pages[a]; + const VirtualTexture::Page& pageB = virtualTexture.pages[b]; + const float ax = (pageA.x + 0.5f) / activeDimension - gCenterX; + const float ay = (pageA.y + 0.5f) / activeDimension - gCenterY; + const float bx = (pageB.x + 0.5f) / activeDimension - gCenterX; + const float by = (pageB.y + 0.5f) / activeDimension - gCenterY; return ax * ax + ay * ay < bx * bx + by * by; }); - // Touch resident pages for LRU accounting and cap misses to avoid a camera - // jump causing an unbounded upload hitch in one frame. + // Touch the whole working set before choosing victims. This ensures a new + // center page cannot evict a still-visible page encountered later below. for (int pageIndex : requested) { VirtualTexture::Page& page = virtualTexture.pages[pageIndex]; if (page.slot >= 0) - { virtualTexture.slots[page.slot].lastUsed = virtualTexture.frame; - } - else if (virtualTexture.uploadsThisFrame < UPLOADS_PER_FRAME) + } + + // Cap misses to avoid a camera jump causing an unbounded upload hitch in + // one frame. Missing detail is covered by the pinned fallback mip. + for (int pageIndex : requested) + { + VirtualTexture::Page& page = virtualTexture.pages[pageIndex]; + if (page.slot < 0 && virtualTexture.uploadsThisFrame < UPLOADS_PER_FRAME) { uploadPage(virtualTexture, pageIndex); ++virtualTexture.uploadsThisFrame; } } - // At 32x32 texels the complete page table is cheap to upload each frame. + // The complete stacked page table is still only 32x63 texels. glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTable); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIRTUAL_PAGES, VIRTUAL_PAGES, + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIRTUAL_PAGES, PAGE_TABLE_HEIGHT, GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); } @@ -494,6 +615,7 @@ void updateTitle(GLFWwindow* window, const VirtualTexture& virtualTexture, { title << " | cache " << virtualTexture.residentCount << "/" << virtualTexture.slots.size() << ", uploads " << virtualTexture.uploadsThisFrame << "/frame" + << ", mip " << virtualTexture.activeMip << ", view " << std::fixed << std::setprecision(1) << gViewSpan * 100.0f << "%"; } title << " | [1-4] mode [WASD/arrows] pan [wheel/Q/E] zoom [R] reset [Esc] quit"; @@ -562,11 +684,19 @@ int main() if (!dxtTexture) dxtTexture = sourceTexture; if (!astcTexture) astcTexture = sourceTexture; + const char* dxtStatus = dxtSupported ? "yes" + : (dxtExtension ? "no (compressed upload rejected; using RGBA8 fallback)" + : "no (extension unavailable; using RGBA8 fallback)"); + const char* astcStatus = astcSupported ? "yes" + : (astcExtension ? "no (compressed upload rejected; using RGBA8 fallback)" + : "no (extension unavailable; using RGBA8 fallback)"); + std::cout << "Texture Lab controls:\n" << " 1: RGBA8 source 2: DXT1/BC1 3: ASTC 4x4 4: virtual texture\n" << " WASD/arrows: pan mouse wheel or Q/E: zoom R: reset Esc: quit\n\n" - << "GPU DXT/S3TC support: " << (dxtSupported ? "yes" : "no (using RGBA8 fallback)") << '\n' - << "GPU ASTC LDR support: " << (astcSupported ? "yes" : "no (using RGBA8 fallback)") << '\n' + << "OpenGL renderer: " << reinterpret_cast(glGetString(GL_RENDERER)) << '\n' + << "GPU DXT/S3TC support: " << dxtStatus << '\n' + << "GPU ASTC LDR support: " << astcStatus << '\n' << "Generated payloads: RGBA8=" << sourcePixels.size() << " bytes, DXT1=" << dxtBlocks.size() << " bytes, ASTC=" << astcBlocks.size() << " bytes\n"; @@ -620,6 +750,7 @@ int main() glClear(GL_COLOR_BUFFER_BIT); shader.use(); shader.setInt("virtualMode", gMode == Mode::Virtual ? 1 : 0); + shader.setInt("activeMip", virtualTexture.activeMip); glUniform2f(glGetUniformLocation(shader.ID, "viewCenter"), gCenterX, gCenterY); shader.setFloat("viewSpan", gViewSpan); From f677d2d3b4f80d40dd7a2a2d014d708b12d28a33 Mon Sep 17 00:00:00 2001 From: Jonas Sorgenfrei Date: Sun, 13 Sep 2026 17:24:25 +0200 Subject: [PATCH 4/4] refactor(texture): split texture samples by topic --- CMakeLists.txt | 4 +- samples/Basics/README.md | 7 +- samples/Basics/Texture/README.md | 32 - samples/Basics/Texture/shader/shader.vert | 11 - samples/Basics/Texture/shader/shaderfs1.frag | 9 - samples/Basics/Texture/shader/shaderfs2.frag | 11 - samples/Basics/Texture/shader/shaderfs3.frag | 11 - .../Basics/Texture/shader/textureShader.frag | 88 -- samples/Basics/Texture/src/main.cpp | 791 ------------------ samples/Basics/Texture_Basics/README.md | 13 + .../Basics/Texture_Basics/shader/texture.frag | 14 + .../shader/texture.vert} | 0 samples/Basics/Texture_Basics/src/main.cpp | 167 ++++ samples/Basics/Texture_Compression/README.md | 21 + .../shader/compression.frag | 11 + .../shader/compression.vert | 12 + .../src/compression_encoders.h | 180 ++++ .../Basics/Texture_Compression/src/main.cpp | 193 +++++ .../Basics/Texture_VirtualTexturing/README.md | 22 + .../shader/virtual_texture.frag | 75 ++ .../shader/virtual_texture.vert | 12 + .../Texture_VirtualTexturing/src/main.cpp | 180 ++++ .../src/virtual_texture.cpp | 249 ++++++ .../src/virtual_texture.h | 69 ++ 24 files changed, 1226 insertions(+), 956 deletions(-) delete mode 100644 samples/Basics/Texture/README.md delete mode 100644 samples/Basics/Texture/shader/shader.vert delete mode 100644 samples/Basics/Texture/shader/shaderfs1.frag delete mode 100644 samples/Basics/Texture/shader/shaderfs2.frag delete mode 100644 samples/Basics/Texture/shader/shaderfs3.frag delete mode 100644 samples/Basics/Texture/shader/textureShader.frag delete mode 100644 samples/Basics/Texture/src/main.cpp create mode 100644 samples/Basics/Texture_Basics/README.md create mode 100644 samples/Basics/Texture_Basics/shader/texture.frag rename samples/Basics/{Texture/shader/textureShader.vert => Texture_Basics/shader/texture.vert} (100%) create mode 100644 samples/Basics/Texture_Basics/src/main.cpp create mode 100644 samples/Basics/Texture_Compression/README.md create mode 100644 samples/Basics/Texture_Compression/shader/compression.frag create mode 100644 samples/Basics/Texture_Compression/shader/compression.vert create mode 100644 samples/Basics/Texture_Compression/src/compression_encoders.h create mode 100644 samples/Basics/Texture_Compression/src/main.cpp create mode 100644 samples/Basics/Texture_VirtualTexturing/README.md create mode 100644 samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.frag create mode 100644 samples/Basics/Texture_VirtualTexturing/shader/virtual_texture.vert create mode 100644 samples/Basics/Texture_VirtualTexturing/src/main.cpp create mode 100644 samples/Basics/Texture_VirtualTexturing/src/virtual_texture.cpp create mode 100644 samples/Basics/Texture_VirtualTexturing/src/virtual_texture.h 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/README.md b/samples/Basics/Texture/README.md deleted file mode 100644 index c7127fe..0000000 --- a/samples/Basics/Texture/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Texture lab - -This sample compares a generated RGBA8 source image with two GPU block-compressed -representations and a software-managed virtual texture. -It starts in virtual-texture mode so page streaming is visible immediately. -It implements [ASTC + DXT Texturing issue #7](https://github.com/jonassorgenfrei/OpenGL/issues/7) -and [Virtual Texturing issue #42](https://github.com/jonassorgenfrei/OpenGL/issues/42). - -| 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 | -| `4` | 2048x2048 virtual texture backed by an 11x11 physical page cache | - -In virtual-texture mode, use `WASD` or the arrow keys to pan, the mouse wheel or -`Q`/`E` to zoom, and `R` to reset the view. The window title reports residency, -upload activity, and the currently requested mip level. - -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 and reports this in both the console and window title. -The console distinguishes an unavailable extension from a rejected compressed -upload and prints the active renderer to make driver capability issues explicit. - -The virtual texture demonstrates a stacked mip page table, bounded physical -storage, LRU replacement, an upload budget, and one-pixel page gutters. It -selects the finest visible mip that fits the streaming cache and permanently -keeps the 2x2 and 1x1 mip levels resident. Missing detailed pages therefore -fall back to a complete coarse image instead of exposing cache holes or -thrashing when the view is zoomed out. The 16 MiB logical RGBA8 image uses -about 2.1 MiB of physical cache and page-table storage. 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 cf0bb4d..0000000 --- a/samples/Basics/Texture/shader/textureShader.frag +++ /dev/null @@ -1,88 +0,0 @@ -#version 330 core - -out vec4 fragColor; -in vec2 texCoord; - -uniform sampler2D displayTexture; -uniform sampler2D physicalCache; -uniform sampler2D pageTable; -uniform bool virtualMode; -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) -{ - // The mip page tables are packed vertically: 32 + 16 + 8 + 4 + 2 + 1. - 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() -{ - if (!virtualMode) - { - fragColor = texture(displayTexture, texCoord); - return; - } - - // Resolve the screen coordinate into logical texture space, then select - // the requested mip or the first coarser page that is already resident. - vec2 virtualUv = viewCenter + (texCoord - 0.5) * viewSpan; - vec4 entry = vec4(0.0); - int sampledMip = activeMip; - for (int mip = 0; mip < 6; ++mip) - { - if (mip < activeMip) continue; - int dimension = pageDimension(mip); - vec2 candidatePosition = virtualUv * float(dimension); - ivec2 page = clamp(ivec2(floor(candidatePosition)), 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; - // Logical-space derivatives keep filtering stable across discontinuous - // physical cache addresses. - vec2 gradientX = dFdx(virtualUv) * (sampledPages * pageSize / cacheSize); - vec2 gradientY = dFdy(virtualUv) * (sampledPages * pageSize / cacheSize); - - // The two coarsest levels are pinned, so this is only a defensive marker - // for a broken page-table mapping rather than normal streaming behavior. - if (entry.b < 0.5) - { - vec2 checkerCell = floor(pagePosition * 4.0); - float checker = mod(checkerCell.x + checkerCell.y, 2.0); - fragColor = vec4(mix(vec3(0.035, 0.04, 0.055), vec3(0.14, 0.045, 0.13), checker), 1.0); - return; - } - - // Remap the texel into its physical slot. Sampling inside the one-texel - // gutter prevents linear filtering from leaking neighboring cache slots. - 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; - vec3 color = textureGrad(physicalCache, cacheUv, gradientX, gradientY).rgb; - - // A subtle border makes page granularity visible for teaching and debugging. - float edgeDistance = min(min(localUv.x, localUv.y), min(1.0 - localUv.x, 1.0 - localUv.y)); - float pageBorder = 1.0 - smoothstep(0.0, 0.018, edgeDistance); - color = mix(color, vec3(0.02), pageBorder * 0.4); - fragColor = vec4(color, 1.0); -} diff --git a/samples/Basics/Texture/src/main.cpp b/samples/Basics/Texture/src/main.cpp deleted file mode 100644 index daa00f4..0000000 --- a/samples/Basics/Texture/src/main.cpp +++ /dev/null @@ -1,791 +0,0 @@ -#include -#include - -#include "modules/filesystem.h" -#include "modules/shader_s.h" -#include "modules/window.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ -// The source image is deliberately small enough to compare formats directly. -// The virtual image is larger, but only CACHE_PAGES squared pages ever reside -// in GPU memory. Each physical page includes a one-texel filtering gutter. -constexpr int WINDOW_WIDTH = 1000; -constexpr int WINDOW_HEIGHT = 700; -constexpr int IMAGE_SIZE = 256; -constexpr int ASTC_BLOCK = 4; -constexpr int VIRTUAL_SIZE = 2048; -constexpr int PAGE_SIZE = 64; -constexpr int VIRTUAL_PAGES = VIRTUAL_SIZE / PAGE_SIZE; -constexpr int VIRTUAL_MIP_LEVELS = 6; // 32, 16, 8, 4, 2, and 1 pages per axis -constexpr int PINNED_MIP = 4; // 2x2 and 1x1 fallbacks never leave memory -constexpr int PAGE_TABLE_HEIGHT = 63; // sum of all mip dimensions -constexpr int CACHE_PAGES = 11; -constexpr int PAGE_GUTTER = 1; -constexpr int SLOT_SIZE = PAGE_SIZE + PAGE_GUTTER * 2; -constexpr int CACHE_SIZE = CACHE_PAGES * SLOT_SIZE; -constexpr int UPLOADS_PER_FRAME = 4; - -enum class Mode { Source, Dxt1, Astc, Virtual }; - -struct VirtualTexture -{ - // Logical pages point into the physical cache. A value of -1 means that - // the shader falls back to a coarser resident mip until this page streams. - struct Page - { - int slot = -1; - int mip = 0; - int x = 0; - int y = 0; - }; - - // Cache slots track ownership and age for least-recently-used replacement. - struct Slot { int page = -1; std::uint64_t lastUsed = 0; }; - - 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; -}; - -struct PageRect -{ - int minX = 0; - int maxX = 0; - int minY = 0; - int maxY = 0; - - int count() const { return (maxX - minX + 1) * (maxY - minY + 1); } -}; - -Mode gMode = Mode::Virtual; -float gCenterX = 0.5f; -float gCenterY = 0.5f; -float gViewSpan = 0.20f; -double gPendingScroll = 0.0; -std::array gPreviousKeys{}; - -float clampFloat(float value, float minimum, float maximum) -{ - return std::max(minimum, std::min(maximum, value)); -} - -int mipPageDimension(int mip) -{ - return std::max(1, VIRTUAL_PAGES >> mip); -} - -int pageTableYOffset(int mip) -{ - int offset = 0; - for (int level = 0; level < mip; ++level) offset += mipPageDimension(level); - return offset; -} - -int mipPageOffset(int mip) -{ - int offset = 0; - for (int level = 0; level < mip; ++level) - { - const int dimension = mipPageDimension(level); - offset += dimension * dimension; - } - return offset; -} - -int pageIndex(int mip, int x, int y) -{ - return mipPageOffset(mip) + y * mipPageDimension(mip) + x; -} - -std::size_t pageTablePixelOffset(const VirtualTexture::Page& page) -{ - const int tableY = pageTableYOffset(page.mip) + page.y; - return static_cast((tableY * VIRTUAL_PAGES + page.x) * 4); -} - -PageRect requestedPageRect(int mip) -{ - const int dimension = mipPageDimension(mip); - const float halfSpan = gViewSpan * 0.5f; - return { - std::max(0, static_cast(std::floor((gCenterX - halfSpan) * dimension)) - 1), - std::min(dimension - 1, static_cast(std::floor((gCenterX + halfSpan) * dimension)) + 1), - std::max(0, static_cast(std::floor((gCenterY - halfSpan) * dimension)) - 1), - std::min(dimension - 1, static_cast(std::floor((gCenterY + halfSpan) * dimension)) + 1) - }; -} - -std::uint8_t toByte(float value) -{ - return static_cast(clampFloat(value, 0.0f, 1.0f) * 255.0f + 0.5f); -} - -// Gradients, a grid, a checker, and rings expose compression artifacts and -// virtual page boundaries more clearly than a smooth source image would. -std::array proceduralColor(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 int cellX = x / std::max(1, size / 16); - const int cellY = y / std::max(1, size / 16); - const float checker = ((cellX + cellY) & 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 int gridStep = std::max(1, size / 8); - const int gridWidth = std::max(1, size / 256); - const bool majorGrid = x % gridStep < gridWidth || y % gridStep < gridWidth; - - float r = 0.10f + 0.70f * u + checker; - float g = 0.10f + 0.70f * v + checker; - float b = 0.16f + 0.34f * rings + checker; - if (majorGrid) r = g = b = 0.96f; - return { toByte(r), toByte(g), toByte(b), 255 }; -} - -std::vector makeSourceImage() -{ - std::vector pixels(IMAGE_SIZE * IMAGE_SIZE * 4); - for (int y = 0; y < IMAGE_SIZE; ++y) - { - for (int x = 0; x < IMAGE_SIZE; ++x) - { - const auto color = proceduralColor(x, y, IMAGE_SIZE); - std::copy(color.begin(), color.end(), pixels.begin() + (y * IMAGE_SIZE + x) * 4); - } - } - return pixels; -} - -// DXT1 stores two RGB565 endpoints and sixteen two-bit palette indices per -// 4x4 block. This teaching encoder favors clarity over exhaustive endpoint search. -std::uint16_t packRgb565(const std::uint8_t* color) -{ - return static_cast(((color[0] >> 3) << 11) | - ((color[1] >> 2) << 5) | - (color[2] >> 3)); -} - -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) - }; -} - -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) - { - // Select a bounding-box endpoint pair for this 4x4 footprint. - 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); - } - - // Four-color mode derives two palette entries between the endpoints. - const auto c0 = unpackRgb565(color0); - const auto c1 = unpackRgb565(color1); - 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 } - }; - - // Assign each texel to its least-squares palette match. - 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) - { - bestError = error; - best = candidate; - } - } - 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 compact educational ASTC encoder using valid LDR void-extent blocks. -// ASTC stores 128 bits per block; this representation averages each 4x4 -// footprint into one RGBA16 constant. Production endpoint/weight searches -// belong in an offline compressor such as astcenc. -std::vector encodeAstc4x4(const std::vector& rgba, int width, int height) -{ - const int blocksX = (width + ASTC_BLOCK - 1) / ASTC_BLOCK; - const int blocksY = (height + ASTC_BLOCK - 1) / ASTC_BLOCK; - 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 < ASTC_BLOCK; ++py) - { - for (int px = 0; px < ASTC_BLOCK; ++px) - { - const int x = std::min(width - 1, bx * ASTC_BLOCK + px); - const int y = std::min(height - 1, by * ASTC_BLOCK + py); - const std::uint8_t* pixel = &rgba[(y * width + x) * 4]; - for (int channel = 0; channel < 4; ++channel) sum[channel] += pixel[channel]; - } - } - - // Bits 10..63 are one, leaving the void extent unspecified. - // Bytes 8..15 contain little-endian RGBA UNORM16 values. - std::uint8_t* block = &output[(by * blocksX + bx) * 16]; - block[0] = 0xfc; // bits 8..0: ASTC void-extent marker - block[1] = 0xfd; // LDR, reserved bits and all extent bits set - 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; -} - -GLuint createRgbaTexture(const std::vector& pixels, int width, int height) -{ - 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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - glGenerateMipmap(GL_TEXTURE_2D); - return texture; -} - -GLuint createCompressedTexture(GLenum format, const std::vector& blocks, int width, int height) -{ - 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 is only the first check. The upload and the texture's - // reported storage state confirm that the driver accepted the format. - while (glGetError() != GL_NO_ERROR) {} - glCompressedTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, - static_cast(blocks.size()), blocks.data()); - GLint storedCompressed = GL_FALSE; - glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &storedCompressed); - if (glGetError() != GL_NO_ERROR || storedCompressed != GL_TRUE) - { - glDeleteTextures(1, &texture); - return 0; - } - return texture; -} - -void initializeVirtualTexture(VirtualTexture& virtualTexture) -{ - // The physical cache owns texels. All mip page tables are stacked vertically - // in one RGBA8 texture; entries store cache-slot XY and residency in blue. - virtualTexture.tablePixels.assign(VIRTUAL_PAGES * PAGE_TABLE_HEIGHT * 4, 0); - for (int mip = 0; mip < VIRTUAL_MIP_LEVELS; ++mip) - { - const int dimension = mipPageDimension(mip); - for (int y = 0; y < dimension; ++y) - for (int x = 0; x < dimension; ++x) - virtualTexture.pages.push_back({ -1, mip, x, y }); - } - - glGenTextures(1, &virtualTexture.cache); - glBindTexture(GL_TEXTURE_2D, virtualTexture.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, CACHE_SIZE, CACHE_SIZE, 0, GL_RGBA, - GL_UNSIGNED_BYTE, nullptr); - - glGenTextures(1, &virtualTexture.pageTable); - glBindTexture(GL_TEXTURE_2D, virtualTexture.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, VIRTUAL_PAGES, PAGE_TABLE_HEIGHT, 0, - GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); -} - -void uploadPage(VirtualTexture& virtualTexture, int pageIndex) -{ - // Prefer a free slot; once full, replace the globally least-recently-used - // slot and invalidate the evicted page's table entry. - int selectedSlot = -1; - std::uint64_t oldest = std::numeric_limits::max(); - for (int slotIndex = 0; slotIndex < static_cast(virtualTexture.slots.size()); ++slotIndex) - { - if (virtualTexture.slots[slotIndex].page < 0) - { - selectedSlot = slotIndex; - break; - } - if (virtualTexture.pages[virtualTexture.slots[slotIndex].page].mip >= PINNED_MIP) - continue; - if (virtualTexture.slots[slotIndex].lastUsed < oldest) - { - oldest = virtualTexture.slots[slotIndex].lastUsed; - selectedSlot = slotIndex; - } - } - // The caller limits streaming pages to the non-pinned capacity, so this - // guard can only trigger if those cache invariants are changed later. - if (selectedSlot < 0) return; - - VirtualTexture::Slot& slot = virtualTexture.slots[selectedSlot]; - if (slot.page >= 0) - { - VirtualTexture::Page& evictedPage = virtualTexture.pages[slot.page]; - evictedPage.slot = -1; - std::fill_n(&virtualTexture.tablePixels[pageTablePixelOffset(evictedPage)], 4, 0); - } - else - { - ++virtualTexture.residentCount; - } - - // Generate the requested page and its border texels on demand. A real - // streamer would usually obtain this payload from disk or a worker thread. - VirtualTexture::Page& requestedPage = virtualTexture.pages[pageIndex]; - const int mipScale = 1 << requestedPage.mip; - std::vector pixels(SLOT_SIZE * SLOT_SIZE * 4); - for (int y = 0; y < SLOT_SIZE; ++y) - { - for (int x = 0; x < SLOT_SIZE; ++x) - { - // Sample the center of the source footprint represented by this - // mip texel. Gutter coordinates naturally fetch adjacent pages. - const int virtualX = (requestedPage.x * PAGE_SIZE + x - PAGE_GUTTER) * mipScale + mipScale / 2; - const int virtualY = (requestedPage.y * PAGE_SIZE + y - PAGE_GUTTER) * mipScale + mipScale / 2; - const auto color = proceduralColor(virtualX, virtualY, VIRTUAL_SIZE); - std::copy(color.begin(), color.end(), pixels.begin() + (y * SLOT_SIZE + x) * 4); - } - } - - // Update only one atlas slot instead of reallocating the physical cache. - const int slotX = selectedSlot % CACHE_PAGES; - const int slotY = selectedSlot / CACHE_PAGES; - glBindTexture(GL_TEXTURE_2D, virtualTexture.cache); - glTexSubImage2D(GL_TEXTURE_2D, 0, slotX * SLOT_SIZE, slotY * SLOT_SIZE, - SLOT_SIZE, SLOT_SIZE, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data()); - - // Publish the new mapping after its texels have reached the cache. - slot.page = pageIndex; - slot.lastUsed = virtualTexture.frame; - requestedPage.slot = selectedSlot; - std::uint8_t* entry = &virtualTexture.tablePixels[pageTablePixelOffset(requestedPage)]; - entry[0] = static_cast(slotX); - entry[1] = static_cast(slotY); - entry[2] = 255; - entry[3] = 255; -} - -void updateVirtualTexture(VirtualTexture& virtualTexture) -{ - ++virtualTexture.frame; - virtualTexture.uploadsThisFrame = 0; - - // Keep the 2x2 and 1x1 mip levels permanently resident. Until a detailed - // page arrives, the shader walks up to one of these complete fallbacks. - if (virtualTexture.frame == 1) - { - for (int mip = PINNED_MIP; mip < VIRTUAL_MIP_LEVELS; ++mip) - { - const int dimension = mipPageDimension(mip); - for (int y = 0; y < dimension; ++y) - for (int x = 0; x < dimension; ++x) - uploadPage(virtualTexture, pageIndex(mip, x, y)); - } - } - - int pinnedPageCount = 0; - for (int mip = PINNED_MIP; mip < VIRTUAL_MIP_LEVELS; ++mip) - { - const int dimension = mipPageDimension(mip); - pinnedPageCount += dimension * dimension; - } - const int streamingCapacity = static_cast(virtualTexture.slots.size()) - pinnedPageCount; - - // Choose the finest level whose visible rectangle and prefetch border fit - // in the streaming portion of the cache. This prevents visible-page churn - // when zooming out instead of blindly requesting hundreds of mip-0 pages. - virtualTexture.activeMip = PINNED_MIP - 1; - PageRect requestedRect = requestedPageRect(virtualTexture.activeMip); - for (int mip = 0; mip < PINNED_MIP; ++mip) - { - const PageRect candidate = requestedPageRect(mip); - if (candidate.count() <= streamingCapacity) - { - virtualTexture.activeMip = mip; - requestedRect = candidate; - break; - } - } - - const int activeDimension = mipPageDimension(virtualTexture.activeMip); - std::vector requested; - requested.reserve(static_cast(requestedRect.count())); - for (int y = requestedRect.minY; y <= requestedRect.maxY; ++y) - for (int x = requestedRect.minX; x <= requestedRect.maxX; ++x) - requested.push_back(pageIndex(virtualTexture.activeMip, x, y)); - - // Stream center-first so the most noticeable holes fill first. - std::sort(requested.begin(), requested.end(), [&](int a, int b) - { - const VirtualTexture::Page& pageA = virtualTexture.pages[a]; - const VirtualTexture::Page& pageB = virtualTexture.pages[b]; - const float ax = (pageA.x + 0.5f) / activeDimension - gCenterX; - const float ay = (pageA.y + 0.5f) / activeDimension - gCenterY; - const float bx = (pageB.x + 0.5f) / activeDimension - gCenterX; - const float by = (pageB.y + 0.5f) / activeDimension - gCenterY; - return ax * ax + ay * ay < bx * bx + by * by; - }); - - // Touch the whole working set before choosing victims. This ensures a new - // center page cannot evict a still-visible page encountered later below. - for (int pageIndex : requested) - { - VirtualTexture::Page& page = virtualTexture.pages[pageIndex]; - if (page.slot >= 0) - virtualTexture.slots[page.slot].lastUsed = virtualTexture.frame; - } - - // Cap misses to avoid a camera jump causing an unbounded upload hitch in - // one frame. Missing detail is covered by the pinned fallback mip. - for (int pageIndex : requested) - { - VirtualTexture::Page& page = virtualTexture.pages[pageIndex]; - if (page.slot < 0 && virtualTexture.uploadsThisFrame < UPLOADS_PER_FRAME) - { - uploadPage(virtualTexture, pageIndex); - ++virtualTexture.uploadsThisFrame; - } - } - - // The complete stacked page table is still only 32x63 texels. - glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTable); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, VIRTUAL_PAGES, PAGE_TABLE_HEIGHT, - GL_RGBA, GL_UNSIGNED_BYTE, virtualTexture.tablePixels.data()); -} - -bool pressedOnce(GLFWwindow* window, int key) -{ - // Edge detection prevents a held key from repeatedly changing modes. - 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_1)) gMode = Mode::Source; - if (pressedOnce(window, GLFW_KEY_2)) gMode = Mode::Dxt1; - if (pressedOnce(window, GLFW_KEY_3)) gMode = Mode::Astc; - if (pressedOnce(window, GLFW_KEY_4)) gMode = Mode::Virtual; - if (pressedOnce(window, GLFW_KEY_R)) - { - gCenterX = gCenterY = 0.5f; - gViewSpan = 0.20f; - } - - 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); -} - -std::string modeName(Mode mode, bool dxtSupported, bool astcSupported) -{ - switch (mode) - { - case Mode::Source: return "RGBA8 source (256 x 256, 256 KiB)"; - case Mode::Dxt1: return dxtSupported ? "DXT1 / BC1 (32 KiB, 8:1)" : "DXT1 unavailable - RGBA8 fallback"; - case Mode::Astc: return astcSupported ? "ASTC 4x4 (64 KiB, 4:1)" : "ASTC unavailable - RGBA8 fallback"; - case Mode::Virtual: return "Virtual texture (2048 x 2048 logical)"; - } - return {}; -} - -void updateTitle(GLFWwindow* window, const VirtualTexture& virtualTexture, - bool dxtSupported, bool astcSupported) -{ - std::ostringstream title; - title << "Texture Lab | " << modeName(gMode, dxtSupported, astcSupported); - if (gMode == Mode::Virtual) - { - title << " | cache " << virtualTexture.residentCount << "/" << virtualTexture.slots.size() - << ", uploads " << virtualTexture.uploadsThisFrame << "/frame" - << ", mip " << virtualTexture.activeMip - << ", view " << std::fixed << std::setprecision(1) << gViewSpan * 100.0f << "%"; - } - title << " | [1-4] mode [WASD/arrows] pan [wheel/Q/E] zoom [R] reset [Esc] quit"; - glfwSetWindowTitle(window, title.str().c_str()); -} - -void framebufferSizeCallback(GLFWwindow*, int width, int height) -{ - glViewport(0, 0, width, height); -} - -void scrollCallback(GLFWwindow*, double, double yOffset) -{ - gPendingScroll += yOffset; -} -} - -int main() -{ - // Compressed formats are optional even though the sample uses a portable - // OpenGL 3.3 context, so all compressed allocations are capability-checked. - 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 Lab", nullptr, nullptr); - if (!window) - { - std::cerr << "Failed to create GLFW window\n"; - glfwTerminate(); - return -1; - } - glfwMakeContextCurrent(window); - glfwSwapInterval(1); - glfwSetFramebufferSizeCallback(window, framebufferSizeCallback); - glfwSetScrollCallback(window, scrollCallback); - - if (!gladLoadGLLoader(reinterpret_cast(glfwGetProcAddress))) - { - std::cerr << "Failed to initialize GLAD\n"; - glfwTerminate(); - return -1; - } - icon(window); - - // Encode the same source into every comparison format. A rejected upload - // safely aliases the original RGBA8 texture as its fallback. - const bool dxtExtension = GLAD_GL_EXT_texture_compression_s3tc != 0; - const bool astcExtension = GLAD_GL_KHR_texture_compression_astc_ldr != 0; - const std::vector sourcePixels = makeSourceImage(); - const std::vector dxtBlocks = encodeDxt1(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); - const std::vector astcBlocks = encodeAstc4x4(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); - const GLuint sourceTexture = createRgbaTexture(sourcePixels, IMAGE_SIZE, IMAGE_SIZE); - GLuint dxtTexture = dxtExtension - ? createCompressedTexture(GL_COMPRESSED_RGB_S3TC_DXT1_EXT, dxtBlocks, IMAGE_SIZE, IMAGE_SIZE) - : 0; - GLuint astcTexture = astcExtension - ? createCompressedTexture(GL_COMPRESSED_RGBA_ASTC_4x4_KHR, astcBlocks, IMAGE_SIZE, IMAGE_SIZE) - : 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 (compressed upload rejected; using RGBA8 fallback)" - : "no (extension unavailable; using RGBA8 fallback)"); - const char* astcStatus = astcSupported ? "yes" - : (astcExtension ? "no (compressed upload rejected; using RGBA8 fallback)" - : "no (extension unavailable; using RGBA8 fallback)"); - - std::cout << "Texture Lab controls:\n" - << " 1: RGBA8 source 2: DXT1/BC1 3: ASTC 4x4 4: virtual texture\n" - << " WASD/arrows: pan mouse wheel or Q/E: zoom R: reset Esc: quit\n\n" - << "OpenGL renderer: " << reinterpret_cast(glGetString(GL_RENDERER)) << '\n' - << "GPU DXT/S3TC support: " << dxtStatus << '\n' - << "GPU ASTC LDR support: " << astcStatus << '\n' - << "Generated payloads: RGBA8=" << sourcePixels.size() << " bytes, DXT1=" << dxtBlocks.size() - << " bytes, ASTC=" << astcBlocks.size() << " bytes\n"; - - // This software-managed indirection does not require a hardware sparse- - // texture extension, which keeps the residency algorithm visible and portable. - VirtualTexture virtualTexture; - initializeVirtualTexture(virtualTexture); - - // A full-screen quad compares the sampling paths without scene distractions. - 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/textureShader.vert").c_str(), - FileSystem::getSamplePath("shader/textureShader.frag").c_str()); - shader.use(); - shader.setInt("displayTexture", 0); - shader.setInt("physicalCache", 1); - shader.setInt("pageTable", 2); - - double previousTime = glfwGetTime(); - double nextTitleUpdate = 0.0; - // Residency is updated before drawing so a new mapping is visible in the - // same frame as its cache upload. - while (!glfwWindowShouldClose(window)) - { - const double time = glfwGetTime(); - const float deltaTime = static_cast(std::min(0.1, time - previousTime)); - previousTime = time; - processInput(window, deltaTime); - if (gMode == Mode::Virtual) updateVirtualTexture(virtualTexture); - - glClearColor(0.025f, 0.03f, 0.045f, 1.0f); - glClear(GL_COLOR_BUFFER_BIT); - shader.use(); - shader.setInt("virtualMode", gMode == Mode::Virtual ? 1 : 0); - shader.setInt("activeMip", virtualTexture.activeMip); - glUniform2f(glGetUniformLocation(shader.ID, "viewCenter"), gCenterX, gCenterY); - shader.setFloat("viewSpan", gViewSpan); - - glActiveTexture(GL_TEXTURE0); - GLuint displayTexture = sourceTexture; - if (gMode == Mode::Dxt1) displayTexture = dxtTexture; - if (gMode == Mode::Astc) displayTexture = astcTexture; - glBindTexture(GL_TEXTURE_2D, displayTexture); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, virtualTexture.cache); - glActiveTexture(GL_TEXTURE2); - glBindTexture(GL_TEXTURE_2D, virtualTexture.pageTable); - glBindVertexArray(vao); - glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr); - - if (time >= nextTitleUpdate) - { - updateTitle(window, virtualTexture, dxtSupported, astcSupported); - nextTitleUpdate = time + 0.2; - } - glfwSwapBuffers(window); - glfwPollEvents(); - } - - // Fallback textures alias sourceTexture; only successful compressed - // allocations own a separate object and may be deleted independently. - if (dxtSupported) glDeleteTextures(1, &dxtTexture); - if (astcSupported) glDeleteTextures(1, &astcTexture); - glDeleteTextures(1, &sourceTexture); - glDeleteTextures(1, &virtualTexture.cache); - glDeleteTextures(1, &virtualTexture.pageTable); - glDeleteVertexArrays(1, &vao); - glDeleteBuffers(1, &vbo); - glDeleteBuffers(1, &ebo); - glDeleteProgram(shader.ID); - glfwTerminate(); - return 0; -} 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/shader/textureShader.vert b/samples/Basics/Texture_Basics/shader/texture.vert similarity index 100% rename from samples/Basics/Texture/shader/textureShader.vert rename to samples/Basics/Texture_Basics/shader/texture.vert 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; +};