diff --git a/examples/40-svt/fs_vt_mip.sc b/examples/40-svt/fs_vt_mip.sc new file mode 100644 index 000000000..450636b35 --- /dev/null +++ b/examples/40-svt/fs_vt_mip.sc @@ -0,0 +1,18 @@ +$input v_texcoord0 + +/* + * Copyright 2011-2018 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause + */ + +#include "../common/common.sh" +#include "virtualtexture.sh" + +void main() +{ + float mipCount = log2(PageTableSize); + float mip = floor(MipLevel(v_texcoord0.xy, VirtualTextureSize) - MipBias); + mip = clamp(mip, 0, mipCount); + vec2 offset = floor(v_texcoord0.xy * PageTableSize); + gl_FragColor = vec4(floor(vec3(offset / exp2(mip), mip)) / 255.0, 1.0); +} diff --git a/examples/40-svt/fs_vt_unlit.sc b/examples/40-svt/fs_vt_unlit.sc new file mode 100644 index 000000000..22c5c5af6 --- /dev/null +++ b/examples/40-svt/fs_vt_unlit.sc @@ -0,0 +1,15 @@ +$input v_texcoord0 + +/* + * Copyright 2011-2018 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause + */ + +#include "../common/common.sh" +#include "virtualtexture.sh" + +void main() +{ + gl_FragColor = VirtualTexture(v_texcoord0.xy); +} + diff --git a/examples/40-svt/makefile b/examples/40-svt/makefile new file mode 100644 index 000000000..171709170 --- /dev/null +++ b/examples/40-svt/makefile @@ -0,0 +1,10 @@ +# +# Copyright 2011-2018 Branimir Karadzic. All rights reserved. +# License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause +# + +BGFX_DIR=../.. +RUNTIME_DIR=$(BGFX_DIR)/examples/runtime +BUILD_DIR=../../.build + +include $(BGFX_DIR)/scripts/shader.mk diff --git a/examples/40-svt/screenshot.png b/examples/40-svt/screenshot.png new file mode 100644 index 000000000..abb827cc5 Binary files /dev/null and b/examples/40-svt/screenshot.png differ diff --git a/examples/40-svt/svt.cpp b/examples/40-svt/svt.cpp new file mode 100644 index 000000000..23311e0ef --- /dev/null +++ b/examples/40-svt/svt.cpp @@ -0,0 +1,371 @@ +/* + * Copyright 2018 Aleš Mlakar. All rights reserved. + * License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause + */ + + /* + * Reference(s): + * - Sparse Virtual Textures by Sean Barrett + * http://silverspaceship.com/src/svt/ + * - Virtual Texture Demo by Brad Blanchard + * http://linedef.com/virtual-texture-demo.html + * - Mars texture + * http://www.celestiamotherlode.net/catalog/mars.php + */ + +#include "common.h" +#include "bgfx_utils.h" +#include "imgui/imgui.h" +#include "camera.h" +#include "bounds.h" +#include "vt.h" + +namespace +{ + +struct PosTexcoordVertex +{ + float m_x; + float m_y; + float m_z; + float m_u; + float m_v; + + static void init() + { + ms_decl + .begin() + .add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float) + .add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float) + .end(); + }; + + static bgfx::VertexDecl ms_decl; +}; + +bgfx::VertexDecl PosTexcoordVertex::ms_decl; + +static const float s_planeScale = 50.0f; + +static PosTexcoordVertex s_vplaneVertices[] = +{ + { -s_planeScale, 0.0f, s_planeScale, 1.0f, 1.0f }, + { s_planeScale, 0.0f, s_planeScale, 1.0f, 0.0f }, + { -s_planeScale, 0.0f, -s_planeScale, 0.0f, 1.0f }, + { s_planeScale, 0.0f, -s_planeScale, 0.0f, 0.0f }, +}; + +static const uint16_t s_planeIndices[] = +{ + 0, 1, 2, + 1, 3, 2, +}; + +class ExampleSVT : public entry::AppI +{ +public: + ExampleSVT(const char* _name, const char* _description) + : entry::AppI(_name, _description) + { + } + + void init(int32_t _argc, const char* const* _argv, uint32_t _width, uint32_t _height) override + { + Args args(_argc, _argv); + + m_width = _width; + m_height = _height; + m_debug = BGFX_DEBUG_TEXT; + m_reset = BGFX_RESET_VSYNC; + + bgfx::Init init; + + init.type = args.m_type; + init.vendorId = args.m_pciId; + init.resolution.width = m_width; + init.resolution.height = m_height; + init.resolution.reset = m_reset; + bgfx::init(init); + + // Enable m_debug text. + bgfx::setDebug(m_debug); + + // Set views clear state (first pass to 0, second pass to some background color) + for (uint16_t i = 0; i < 2; ++i) + { + bgfx::setViewClear(i + , BGFX_CLEAR_COLOR | BGFX_CLEAR_DEPTH + , i == 0 ? 0 : 0x101050ff + , 1.0f + , 0 + ); + } + + // Create vertex stream declaration. + PosTexcoordVertex::init(); + + // Create static vertex buffer. + m_vbh = bgfx::createVertexBuffer( + bgfx::makeRef(s_vplaneVertices, sizeof(s_vplaneVertices)) + , PosTexcoordVertex::ms_decl + ); + + m_ibh = bgfx::createIndexBuffer( + bgfx::makeRef(s_planeIndices, sizeof(s_planeIndices)) + ); + + // Create program from shaders. + m_vt_unlit = loadProgram("vs_vt_generic", "fs_vt_unlit"); + m_vt_mip = loadProgram("vs_vt_generic", "fs_vt_mip"); + + // Imgui. + imguiCreate(); + + m_timeOffset = bx::getHPCounter(); + + // Get renderer capabilities info. + m_caps = bgfx::getCaps(); + + m_scrollArea = 0; + + // Create and setup camera + cameraCreate(); + + cameraSetPosition({ 0.0f, 5.0f, 0.0f }); + cameraSetVerticalAngle(0.0f); + + // Create Virtual texture info + m_vti = new vt::VirtualTextureInfo(); + m_vti->m_virtualTextureSize = 8192; // The actual size will be read from the tile data file + m_vti->m_tileSize = 128; + m_vti->m_borderSize = 1; + + // Generate tile data file (if not yet created) + { + vt::TileGenerator tileGenerator(m_vti); + tileGenerator.generate("textures/8k_mars.jpg"); + } + + // Load tile data file + auto tileDataFile = new vt::TileDataFile("textures/8k_mars.jpg.cache", m_vti); + tileDataFile->readInfo(); + + // Create virtual texture and feedback buffer + m_vt = new vt::VirtualTexture(tileDataFile, m_vti, 2048, 1); + m_feedbackBuffer = new vt::FeedbackBuffer(m_vti, 64, 64); + + } + + virtual int shutdown() override + { + // Cleanup. + bgfx::frame(); + + cameraDestroy(); + imguiDestroy(); + + bgfx::destroy(m_ibh); + bgfx::destroy(m_vbh); + + bgfx::destroy(m_vt_unlit); + bgfx::destroy(m_vt_mip); + + delete m_vti; + delete m_vt; + delete m_feedbackBuffer; + + // Shutdown bgfx. + bgfx::shutdown(); + + return 0; + } + + bool update() override + { + if (!entry::processEvents(m_width, m_height, m_debug, m_reset, &m_mouseState)) + { + imguiBeginFrame( + m_mouseState.m_mx + , m_mouseState.m_my + , (m_mouseState.m_buttons[entry::MouseButton::Left] ? IMGUI_MBUT_LEFT : 0) + | (m_mouseState.m_buttons[entry::MouseButton::Right] ? IMGUI_MBUT_RIGHT : 0) + | (m_mouseState.m_buttons[entry::MouseButton::Middle] ? IMGUI_MBUT_MIDDLE : 0) + , m_mouseState.m_mz + , uint16_t(m_width) + , uint16_t(m_height) + ); + + showExampleDialog(this); + + int64_t now = bx::getHPCounter(); + static int64_t last = now; + const int64_t frameTime = now - last; + last = now; + const double freq = double(bx::getHPFrequency()); + const float deltaTime = float(frameTime / freq); + + float time = (float)((now - m_timeOffset) / freq); + + if ((BGFX_CAPS_TEXTURE_BLIT | BGFX_CAPS_TEXTURE_READ_BACK) != (bgfx::getCaps()->supported & (BGFX_CAPS_TEXTURE_BLIT | BGFX_CAPS_TEXTURE_READ_BACK))) + { + // When texture read-back or blit is not supported by GPU blink! + bool blink = uint32_t(time*3.0f) & 1; + bgfx::dbgTextPrintf(0, 0, blink ? 0x4f : 0x04, " Texture read-back and/or blit not supported by GPU. "); + + // Set view 0 default viewport. + bgfx::setViewRect(0, 0, 0, uint16_t(m_width), uint16_t(m_height)); + + // This dummy draw call is here to make sure that view 0 is cleared + // if no other draw calls are submitted to view 0. + bgfx::touch(0); + } + else + { + ImGui::SetNextWindowPos( + ImVec2(m_width - m_width / 5.0f - 10.0f, 10.0f) + , ImGuiCond_FirstUseEver + ); + ImGui::SetNextWindowSize( + ImVec2(m_width / 5.0f, m_height - 10.0f) + , ImGuiCond_FirstUseEver + ); + ImGui::Begin("Settings" + , NULL + , 0 + ); + + //ImGui::SliderFloat("intensity", &m_intensity, 0.0f, 3.0f); + auto showBorders = m_vt->isShowBoardersEnabled(); + if (ImGui::Checkbox("Show borders", &showBorders)) + { + m_vt->enableShowBoarders(showBorders); + } + auto colorMipLevels = m_vt->isColorMipLevelsEnabled(); + if (ImGui::Checkbox("Color mip levels", &colorMipLevels)) + { + m_vt->enableColorMipLevels(colorMipLevels); + } + auto uploadsperframe = m_vt->getUploadsPerFrame(); + if (ImGui::InputInt("Updates per frame", &uploadsperframe, 1, 2)) + { + uploadsperframe = bx::clamp(uploadsperframe, 1, 100); + m_vt->setUploadsPerFrame(uploadsperframe); + } + + ImGui::ImageButton(m_vt->getAtlastTexture(), ImVec2(m_width / 5.0f - 16.0f, m_width / 5.0f - 16.0f)); + ImGui::ImageButton(bgfx::getTexture(m_feedbackBuffer->getFrameBuffer()), ImVec2(m_width / 5.0f - 16.0f, m_width / 5.0f - 16.0f)); + + ImGui::End(); + + // Update camera. + cameraUpdate(deltaTime, m_mouseState); + + float view[16]; + cameraGetViewMtx(view); + + float proj[16]; + bx::mtxProj(proj, 60.0f, float(m_width) / float(m_height), 0.1f, 1000.0f, m_caps->homogeneousDepth); + + // Setup views + for (uint16_t i = 0; i < 2; ++i) + { + uint16_t viewWidth = 0; + uint16_t viewHeight = 0; + // Setup pass, first pass is into mip-map feedback buffer, second pass is on screen + if (i == 0) + { + bgfx::setViewFrameBuffer(i, m_feedbackBuffer->getFrameBuffer()); + viewWidth = uint16_t(m_feedbackBuffer->getWidth()); + viewHeight = uint16_t(m_feedbackBuffer->getHeight()); + } + else + { + bgfx::FrameBufferHandle invalid = BGFX_INVALID_HANDLE; + bgfx::setViewFrameBuffer(i, invalid); + viewWidth = uint16_t(m_width); + viewHeight = uint16_t(m_height); + } + + bgfx::setViewRect(i, 0, 0, viewWidth, viewHeight); + bgfx::setViewTransform(i, view, proj); + + float mtx[16]; + bx::mtxIdentity(mtx); + + // Set identity transform for draw call. + bgfx::setTransform(mtx); + + // Set vertex and index buffer. + bgfx::setVertexBuffer(0, m_vbh); + bgfx::setIndexBuffer(m_ibh); + + // Set render states. + bgfx::setState(0 + | BGFX_STATE_WRITE_RGB + | BGFX_STATE_WRITE_A + | BGFX_STATE_WRITE_Z + | BGFX_STATE_DEPTH_TEST_LESS + ); + + // Set virtual texture uniforms + m_vt->setUniforms(); + + // Submit primitive for rendering to first pass (to feedback buffer, where mip levels and tile x/y will be rendered + if (i == 0) + { + bgfx::submit(i, m_vt_mip); + // Download previous frame feedback info + m_feedbackBuffer->download(); + // Update and upload new requests + m_vt->update(m_feedbackBuffer->getRequests(), 4); + // Clear feedback + m_feedbackBuffer->clear(); + // Copy new frame feedback buffer + m_feedbackBuffer->copy(3); + } + else + { + // Submit primitive for rendering to second pass (to back buffer, where virtual texture page table and atlas will be used) + bgfx::submit(i, m_vt_unlit); + } + } + } + + imguiEndFrame(); + + // Advance to next frame. Rendering thread will be kicked to + // process submitted rendering primitives. + bgfx::frame(); + + return true; + } + + return false; + } + + bgfx::VertexBufferHandle m_vbh; + bgfx::IndexBufferHandle m_ibh; + + bgfx::ProgramHandle m_vt_unlit; + bgfx::ProgramHandle m_vt_mip; + + uint32_t m_width; + uint32_t m_height; + uint32_t m_debug; + uint32_t m_reset; + + int32_t m_scrollArea; + + entry::MouseState m_mouseState; + + const bgfx::Caps* m_caps; + int64_t m_timeOffset; + + vt::VirtualTextureInfo* m_vti; + vt::VirtualTexture* m_vt; + vt::FeedbackBuffer* m_feedbackBuffer; +}; + +} // namespace + +ENTRY_IMPLEMENT_MAIN(ExampleSVT, "40-svt", "Sparse Virtual Textures."); diff --git a/examples/40-svt/varying.def.sc b/examples/40-svt/varying.def.sc new file mode 100644 index 000000000..b7b69d9f3 --- /dev/null +++ b/examples/40-svt/varying.def.sc @@ -0,0 +1,4 @@ +vec2 v_texcoord0 : TEXCOORD0 = vec2(0.0, 0.0); + +vec3 a_position : POSITION; +vec2 a_texcoord0 : TEXCOORD0; diff --git a/examples/40-svt/virtualtexture.sh b/examples/40-svt/virtualtexture.sh new file mode 100644 index 000000000..4b210a337 --- /dev/null +++ b/examples/40-svt/virtualtexture.sh @@ -0,0 +1,80 @@ +/* + * Copyright 2011-2018 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause + */ + +uniform vec4 u_vt_settings_1; +uniform vec4 u_vt_settings_2; + +#define VirtualTextureSize u_vt_settings_1.x +#define AtlasScale u_vt_settings_1.y +#define BorderScale u_vt_settings_1.z +#define BorderOffset u_vt_settings_1.w + +#define MipBias u_vt_settings_2.x +#define PageTableSize u_vt_settings_2.y + +SAMPLER2D(s_vt_page_table, 0); +SAMPLER2D(s_vt_texture_atlas, 1); + +// This function estimates mipmap levels +float MipLevel( vec2 uv, float size ) +{ + vec2 dx = dFdx( uv * size ); + vec2 dy = dFdy( uv * size ); + float d = max( dot( dx, dx ), dot( dy, dy ) ); + + return max( 0.5 * log2( d ), 0 ); +} + +// This function samples the page table and returns the page's +// position and mip level. +vec3 SampleTable( vec2 uv, float mip ) +{ + vec2 offset = fract( uv * PageTableSize ) / PageTableSize; + return texture2DLod( s_vt_page_table, uv - offset, mip ).xyz; +} + +// This functions samples from the texture atlas and returns the final color +vec4 SampleAtlas( vec3 page, vec2 uv ) +{ + float mipsize = exp2( floor( page.z * 255.0 + 0.5 ) ); + + uv = fract( uv * PageTableSize / mipsize ); + + uv *= BorderScale; + uv += BorderOffset; + + vec2 offset = floor( page.xy * 255 + 0.5 ); + + return texture2D( s_vt_texture_atlas, ( offset + uv ) * AtlasScale ); +} + +// Ugly brute force trilinear, look up twice and mix +vec4 VirtualTextureTrilinear( vec2 uv ) +{ + float miplevel = MipLevel( uv, VirtualTextureSize ); + miplevel = clamp( miplevel, 0, log2( PageTableSize )-1 ); + + float mip1 = floor( miplevel ); + float mip2 = mip1 + 1; + float mipfrac = miplevel - mip1; + + vec3 page1 = SampleTable( uv, mip1 ); + vec3 page2 = SampleTable( uv, mip2 ); + + vec4 sample1 = SampleAtlas( page1, uv ); + vec4 sample2 = SampleAtlas( page2, uv ); + + return mix( sample1, sample2, mipfrac ); +} + +// Simple bilinear +vec4 VirtualTexture( vec2 uv ) +{ + float mip = floor( MipLevel( uv, VirtualTextureSize ) ); + mip = clamp( mip, 0, log2( PageTableSize ) ); + + vec3 page = SampleTable( uv, mip ); + return SampleAtlas( page, uv ); +} diff --git a/examples/40-svt/vs_vt_generic.sc b/examples/40-svt/vs_vt_generic.sc new file mode 100644 index 000000000..cd0a692cf --- /dev/null +++ b/examples/40-svt/vs_vt_generic.sc @@ -0,0 +1,16 @@ +$input a_position, a_texcoord0 +$output v_texcoord0 + +/* + * Copyright 2011-2018 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bgfx#license-bsd-2-clause + */ + +#include "../common/common.sh" + +void main() +{ + vec3 wpos = mul(u_model[0], vec4(a_position, 1.0) ).xyz; + gl_Position = mul(u_viewProj, vec4(wpos, 1.0) ); + v_texcoord0 = a_texcoord0; +} diff --git a/examples/40-svt/vt.cpp b/examples/40-svt/vt.cpp new file mode 100644 index 000000000..4851bc927 --- /dev/null +++ b/examples/40-svt/vt.cpp @@ -0,0 +1,1248 @@ +#include "vt.h" +#include +#include + +namespace vt +{ + +// Constants +static const int ChannelCount = 4; +static const int StagingTextureCount = 1; +static const int TileFileDataOffset = sizeof(VirtualTextureInfo); + +// Page +uint64_t Page::hash() const +{ + return (uint64_t(m_mip) << 32) | uint64_t((uint32_t(m_x) << 16) | uint32_t(m_y)); +} + +bool Page::operator==(const Page& page) const +{ + return hash() == page.hash(); +} + +bool Page::operator<(const Page& page) const +{ + return hash() < page.hash(); +} + +// PageCount +PageCount::PageCount(Page _page, int _count) + : m_page(_page) + , m_count(_count) +{ +} + +int PageCount::compareTo(const PageCount& other) const +{ + #define Comparer(a, b) bx::clamp(a - b, -1, 1) + if (other.m_page.m_mip != m_page.m_mip) + { + return Comparer(other.m_page.m_mip, m_page.m_mip); + } + return Comparer(other.m_count, m_count); + #undef Comparer +} + +bool PageCount::operator==(const PageCount& other) const +{ + return compareTo(other) == 0; +} + +bool PageCount::operator<(const PageCount& other) const +{ + return compareTo(other) < 0; +} + +// VirtualTextureInfo +VirtualTextureInfo::VirtualTextureInfo() + : m_virtualTextureSize(0) + , m_tileSize(0) + , m_borderSize(0) +{ +} +int VirtualTextureInfo::GetPageSize() const +{ + return m_tileSize + 2 * m_borderSize; +} + +int VirtualTextureInfo::GetPageTableSize() const +{ + return m_virtualTextureSize / m_tileSize; +} + +// StagingPool +StagingPool::StagingPool(int _width, int _height, int _count, bool _readBack) : m_stagingTextureIndex(0), m_width(_width), m_height(_height), m_flags(0) +{ + m_flags = BGFX_TEXTURE_BLIT_DST | BGFX_SAMPLER_UVW_CLAMP; + if (_readBack) + { + m_flags |= BGFX_TEXTURE_READ_BACK; + } + grow(_count); +} + +StagingPool::~StagingPool() +{ + for (int i = 0; i < m_stagingTextures.size(); ++i) + { + bgfx::destroy(m_stagingTextures[i]); + } +} + +void StagingPool::grow(int count) +{ + while (m_stagingTextures.size() < count) + { + auto stagingTexture = bgfx::createTexture2D((uint16_t)m_width, (uint16_t)m_height, false, 1, bgfx::TextureFormat::BGRA8, m_flags); + m_stagingTextures.push_back(stagingTexture); + } +} + +bgfx::TextureHandle StagingPool::getTexture() +{ + return m_stagingTextures[m_stagingTextureIndex]; +} + +void StagingPool::next() +{ + m_stagingTextureIndex = (m_stagingTextureIndex + 1) % m_stagingTextures.size(); +} + +// PageIndexer +PageIndexer::PageIndexer(VirtualTextureInfo* _info) + : m_info(_info) +{ + m_mipcount = (int)(log2(m_info->GetPageTableSize()) + 1); + + m_sizes.resize(m_mipcount); + for (int i = 0; i < m_mipcount; ++i) + { + m_sizes[i] = (m_info->m_virtualTextureSize / m_info->m_tileSize) >> i; + } + + m_offsets.resize(m_mipcount); + m_count = 0; + for (int i = 0; i < m_mipcount; ++i) + { + m_offsets[i] = m_count; + m_count += m_sizes[i] * m_sizes[i]; + } + + // Calculate reverse mapping + m_reverse.resize(m_count); + for (int i = 0; i < m_mipcount; ++i) + { + int size = m_sizes[i]; + for (int y = 0; y < size; ++y) + { + for (int x = 0; x < size; ++x) + { + Page page = { x, y, i }; + m_reverse[getIndexFromPage(page)] = page; + } + } + } +} + +int PageIndexer::getIndexFromPage(Page page) +{ + int offset = m_offsets[page.m_mip]; + int stride = m_sizes[page.m_mip]; + + return offset + page.m_y * stride + page.m_x; +} + +Page PageIndexer::getPageFromIndex(int index) +{ + return m_reverse[index]; +} + +bool PageIndexer::isValid(Page page) +{ + if (page.m_mip < 0) + { + return false; + } + + else if (page.m_mip >= m_mipcount) + { + return false; + } + + if (page.m_x < 0) + { + return false; + } + else if (page.m_x >= m_sizes[page.m_mip]) + { + return false; + } + + if (page.m_y < 0) + { + return false; + } + else if (page.m_y >= m_sizes[page.m_mip]) + { + return false; + } + + return true; +} + +int PageIndexer::getCount() const +{ + return m_count; +} + +// SimpleImage +SimpleImage::SimpleImage(int _width, int _height, int _channelCount, uint8_t _clearValue) + : m_width(_width) + , m_height(_height) + , m_channelCount(_channelCount) +{ + m_data.resize(m_width * m_height * m_channelCount); + clear(_clearValue); +} + +SimpleImage::SimpleImage(int _width, int _height, int _channelCount, std::vector& _data) + : m_width(_width) + , m_height(_height) + , m_channelCount(_channelCount) +{ + m_data = _data; +} + +void SimpleImage::copy(Point dest_offset, SimpleImage& src, Rect src_rect) +{ + int width = bx::min(m_width - dest_offset.m_x, src_rect.m_width); + int height = bx::min(m_height - dest_offset.m_y, src_rect.m_height); + int channels = bx::min(m_channelCount, src.m_channelCount); + + for (int j = 0; j < height; ++j) + { + for (int i = 0; i < width; ++i) + { + int i1 = ((j + dest_offset.m_y) * m_width + (i + dest_offset.m_x)) * m_channelCount; + int i2 = ((j + src_rect.m_y) * src.m_width + (i + src_rect.m_x)) * src.m_channelCount; + for (int c = 0; c < channels; ++c) + { + m_data[i1 + c] = src.m_data[i2 + c]; + } + } + } +} + +void SimpleImage::clear(uint8_t clearValue) +{ + bx::memSet(&m_data[0], clearValue, m_width * m_height * m_channelCount); +} + +void SimpleImage::fill(Rect rect, uint8_t r, uint8_t g, uint8_t b, uint8_t a) +{ + for (int y = rect.minY(); y < rect.maxY(); ++y) + { + for (int x = rect.minX(); x < rect.maxX(); ++x) + { + m_data[m_channelCount * (y * m_width + x) + 0] = b; + m_data[m_channelCount * (y * m_width + x) + 1] = g; + m_data[m_channelCount * (y * m_width + x) + 2] = r; + m_data[m_channelCount * (y * m_width + x) + 3] = a; + } + } +} + +void SimpleImage::mipmap(uint8_t* source, int size, int channels, uint8_t* dest) +{ + int mipsize = size / 2; + + for (int y = 0; y < mipsize; ++y) + { + for (int x = 0; x < mipsize; ++x) + { + for (int c = 0; c < channels; ++c) + { + int index = channels * ((y * 2) * size + (x * 2)) + c; + int sum_value = 4 >> 1; + sum_value += source[index + channels * (0 * size + 0)]; + sum_value += source[index + channels * (0 * size + 1)]; + sum_value += source[index + channels * (1 * size + 0)]; + sum_value += source[index + channels * (1 * size + 1)]; + dest[channels * (y * mipsize + x) + c] = (uint8_t)(sum_value / 4); + } + } + } +} + +// Quadtree +Quadtree::Quadtree(Rect _rect, int _level) + : m_rectangle(_rect) + , m_level(_level) +{ + for (int i = 0; i < 4; ++i) + { + m_children[i] = nullptr; + } +} + +Quadtree::~Quadtree() +{ + for (int i = 0; i < 4; ++i) + { + if (m_children[i] != nullptr) + { + delete m_children[i]; + } + } +} + +void Quadtree::add(Page request, Point mapping) +{ + int scale = 1 << request.m_mip; // Same as pow( 2, mip ) + int x = request.m_x * scale; + int y = request.m_y * scale; + + Quadtree* node = this; + + while (request.m_mip < node->m_level) + { + for (int i = 0; i < 4; ++i) + { + auto rect = node->getRectangle(i); + if (rect.contains({ x, y })) + { + // Create a new one if needed + if (node->m_children[i] == nullptr) + { + node->m_children[i] = new Quadtree(rect, node->m_level - 1); + node = node->m_children[i]; + break; + } + // Otherwise traverse the tree + else + { + node = node->m_children[i]; + break; + } + } + } + } + + // We have created the correct node, now set the mapping + node->m_mapping = mapping; +} + +void Quadtree::remove(Page request) +{ + int index; + auto node = findPage(this, request, &index); + + if (node != nullptr) + { + delete node->m_children[index]; + node->m_children[index] = nullptr; + } +} + +void Quadtree::write(SimpleImage& image, int miplevel) +{ + write(this, image, miplevel); +} + +// Static Functions +Rect Quadtree::getRectangle(int index) +{ + int x = m_rectangle.m_x; + int y = m_rectangle.m_y; + int w = m_rectangle.m_width / 2; + int h = m_rectangle.m_width / 2; + + switch (index) + { + case 0: + return { x, y, w, h }; + case 1: + return { x + w, y, w, h }; + case 2: + return { x + w, y + h, w, h }; + case 3: + return { x, y + h, w, h }; + } + return { 0,0,0,0 }; +} + +void Quadtree::write(Quadtree* node, SimpleImage& image, int miplevel) +{ + if (node->m_level >= miplevel) + { + int rx = node->m_rectangle.m_x >> miplevel; + int ry = node->m_rectangle.m_y >> miplevel; + int rw = node->m_rectangle.m_width >> miplevel; + int rh = node->m_rectangle.m_width >> miplevel; + + image.fill({ rx, ry, rw, rh }, (uint8_t)node->m_mapping.m_x, (uint8_t)node->m_mapping.m_y, (uint8_t)node->m_level, 255); + + for (int i = 0; i < 4; ++i) + { + auto child = node->m_children[i]; + if (child != nullptr) + { + Quadtree::write(child, image, miplevel); + } + } + } +} + +Quadtree* Quadtree::findPage(Quadtree* node, Page request, int* index) +{ + int scale = 1 << request.m_mip; // Same as pow( 2, mip ) + int x = request.m_x * scale; + int y = request.m_y * scale; + + // Find the parent of the child we want to remove + bool exitloop = false; + while (!exitloop) + { + exitloop = true; + for (int i = 0; i < 4; ++i) + { + if (node->m_children[i] != nullptr && node->m_children[i]->m_rectangle.contains({ x, y })) + { + // We found it + if (request.m_mip == node->m_level - 1) + { + *index = i; + return node; + } + // Check the children + else + { + node = node->m_children[i]; + exitloop = false; + } + } + } + } + + // We couldn't find it so it must not exist anymore + *index = -1; + return nullptr; +} + +// PageTable +PageTable::PageTable(PageCache* _cache, VirtualTextureInfo* _info, PageIndexer* _indexer) + : m_info(_info) + , m_indexer(_indexer) + , m_quadtree(nullptr) + , m_quadtreeDirty(true) // Force quadtree dirty on startup +{ + int size = m_info->GetPageTableSize(); + m_quadtree = new Quadtree({ 0, 0, size, size }, (int)log2(size)); + m_texture = bgfx::createTexture2D((uint16_t)size, (uint16_t)size, true, 1, bgfx::TextureFormat::BGRA8, BGFX_SAMPLER_UVW_CLAMP | BGFX_SAMPLER_POINT); + + _cache->added = [=](Page page, Point pt) { m_quadtreeDirty = true; m_quadtree->add(page, pt); }; + _cache->removed = [=](Page page, Point pt) { m_quadtreeDirty = true; m_quadtree->remove(page); pt; }; + + int PageTableSizeLog2 = (int)log2(m_info->GetPageTableSize()); + + for (int i = 0; i < PageTableSizeLog2 + 1; ++i) + { + int mipSize = m_info->GetPageTableSize() >> i; + auto simpleImage = new SimpleImage(mipSize, mipSize, ChannelCount); + auto stagingTexture = bgfx::createTexture2D((uint16_t)mipSize, (uint16_t)mipSize, false, 1, bgfx::TextureFormat::BGRA8, BGFX_SAMPLER_UVW_CLAMP | BGFX_SAMPLER_POINT); + m_images.push_back(simpleImage); + m_stagingTextures.push_back(stagingTexture); + } +} + +PageTable::~PageTable() +{ + delete m_quadtree; + bgfx::destroy(m_texture); + for (int i = 0; i < m_images.size(); ++i) + { + delete m_images[i]; + } + for (int i = 0; i < m_stagingTextures.size(); ++i) + { + bgfx::destroy(m_stagingTextures[i]); + } +} + +void PageTable::update(bgfx::ViewId blitViewId) +{ + if (!m_quadtreeDirty) + { + return; + } + m_quadtreeDirty = false; + int PageTableSizeLog2 = (int)log2(m_info->GetPageTableSize()); + for (int i = 0; i < PageTableSizeLog2 + 1; ++i) + { + m_quadtree->write(*m_images[i], i); + auto stagingTexture = m_stagingTextures[i]; + auto size = uint16_t(m_info->GetPageTableSize() >> i); + bgfx::updateTexture2D(stagingTexture, 0, 0, 0, 0, size, size, bgfx::copy(&m_images[i]->m_data[0], size * size * ChannelCount)); + bgfx::blit(blitViewId, m_texture, uint8_t(i), 0, 0, 0, stagingTexture, 0, 0, 0, 0, size, size); + } +} + +bgfx::TextureHandle PageTable::getTexture() +{ + return m_texture; +} + +// PageLoader +PageLoader::PageLoader(TileDataFile* _tileDataFile, PageIndexer* _indexer, VirtualTextureInfo* _info) + : m_tileDataFile(_tileDataFile) + , m_indexer(_indexer) + , m_info(_info) + , m_colorMipLevels(false) + , m_showBorders(false) +{ +} + +void PageLoader::submit(Page request) +{ + ReadState state; + state.m_page = request; + loadPage(state); + onPageLoadComplete(state); +} + +void PageLoader::loadPage(ReadState& state) +{ + int size = m_info->GetPageSize() * m_info->GetPageSize() * ChannelCount; + state.m_data.resize(size); + if (m_colorMipLevels) + { + copyColor(&state.m_data[0], state.m_page); + } + else if (m_tileDataFile != nullptr) + { + m_tileDataFile->readPage(m_indexer->getIndexFromPage(state.m_page), &state.m_data[0]); + } + if (m_showBorders) + { + copyBorder(&state.m_data[0]); + } +} + +void PageLoader::onPageLoadComplete(ReadState& state) +{ + loadComplete(state.m_page, &state.m_data[0]); +} + +void PageLoader::copyBorder(uint8_t* image) +{ + int pagesize = m_info->GetPageSize(); + int bordersize = m_info->m_borderSize; + + for (int i = 0; i < pagesize; ++i) + { + int xindex = bordersize * pagesize + i; + image[xindex * ChannelCount + 0] = 0; + image[xindex * ChannelCount + 1] = 255; + image[xindex * ChannelCount + 2] = 0; + image[xindex * ChannelCount + 3] = 255; + + int yindex = i * pagesize + bordersize; + image[yindex * ChannelCount + 0] = 0; + image[yindex * ChannelCount + 1] = 255; + image[yindex * ChannelCount + 2] = 0; + image[yindex * ChannelCount + 3] = 255; + } +} + +void PageLoader::copyColor(uint8_t* image, Page request) +{ + static const Color colors[] = + { + {0, 0, 255, 255}, + {0, 255, 255, 255}, + {255, 0, 0, 255}, + {255, 0, 255, 255}, + {255, 255, 0, 255}, + {64, 64, 192, 255}, + {64, 192, 64, 255}, + {64, 192, 192, 255}, + {192, 64, 64, 255}, + {192, 64, 192, 255}, + {192, 192, 64, 255}, + {0, 255, 0, 255} + }; + + int pagesize = m_info->GetPageSize(); + + for (int y = 0; y < pagesize; ++y) + { + for (int x = 0; x < pagesize; ++x) + { + image[(y * pagesize + x) * ChannelCount + 0] = colors[request.m_mip].m_b; + image[(y * pagesize + x) * ChannelCount + 1] = colors[request.m_mip].m_g; + image[(y * pagesize + x) * ChannelCount + 2] = colors[request.m_mip].m_r; + image[(y * pagesize + x) * ChannelCount + 3] = colors[request.m_mip].m_a; + } + } +} + +PageCache::PageCache(VirtualTextureInfo* _info, TextureAtlas* _atlas, PageLoader* _loader, PageIndexer* _indexer, int _count) + : m_info(_info) + , m_atlas(_atlas) + , m_loader(_loader) + , m_indexer(_indexer) + , m_count(_count) +{ + clear(); + m_loader->loadComplete = [&](Page page, uint8_t* data) { loadComplete(page, data); }; +} + +// Update the pages's position in the lru +bool PageCache::touch(Page page) +{ + if (m_loading.find(page) == m_loading.end()) + { + if (m_lru_used.find(page) != m_lru_used.end()) + { + // Find the page (slow!!) and add it to the back of the list + auto it = std::find(m_lru.begin(), m_lru.end(), page); + auto lruPage = *it; + m_lru.erase(it); + m_lru.push_back(lruPage); + return true; + } + } + + return false; +} + +// Schedule a load if not already loaded or loading +bool PageCache::request(Page request, bgfx::ViewId blitViewId) +{ + m_blitViewId = blitViewId; + if (m_loading.find(request) == m_loading.end()) + { + if (m_lru_used.find(request) == m_lru_used.end()) + { + m_loading.insert(request); + m_loader->submit(request); + return true; + } + } + + return false; +} + +void PageCache::clear() +{ + for (auto& lru_page : m_lru) + { + if (m_lru_used.find(lru_page.m_page) != m_lru_used.end()) + { + removed(lru_page.m_page, lru_page.m_point); + } + } + m_lru_used.clear(); + m_lru.clear(); + m_lru.reserve(m_count * m_count); + m_current = 0; +} + +void PageCache::loadComplete(Page page, uint8_t* data) +{ + m_loading.erase(page); + + // Find a place in the atlas for the data + Point pt; + + if (m_current == m_count * m_count) + { + // Remove the oldest lru page and remember it's location so we can use it + auto lru_page = m_lru[0]; + m_lru.erase(m_lru.begin()); + m_lru_used.erase(lru_page.m_page); + pt = lru_page.m_point; + // Notify that we removed a page + removed(lru_page.m_page, lru_page.m_point); + } + else + { + pt = { m_current % m_count, m_current / m_count }; + ++m_current; + + if (m_current == m_count * m_count) + { + bx::debugPrintf("Atlas is full!"); + } + } + + // Notify atlas that he can upload the page and add the page to lru + m_atlas->uploadPage(pt, data, m_blitViewId); + m_lru.push_back({ page, pt }); + m_lru_used.insert(page); + + // Signal that we added a page + added(page, pt); +} + +// TextureAtlas +TextureAtlas::TextureAtlas(VirtualTextureInfo* _info, int _count, int _uploadsperframe) + : m_info(_info) + , m_stagingPool(_info->GetPageSize(), _info->GetPageSize(), _uploadsperframe, false) +{ + // Create atlas texture + int pagesize = m_info->GetPageSize(); + int size = _count * pagesize; + m_texture = bgfx::createTexture2D((uint16_t)size, (uint16_t)size, false, 1, bgfx::TextureFormat::BGRA8, BGFX_SAMPLER_UVW_CLAMP); +} + +TextureAtlas::~TextureAtlas() +{ + bgfx::destroy(m_texture); +} + +void TextureAtlas::setUploadsPerFrame(int count) +{ + m_stagingPool.grow(count); +} + +void TextureAtlas::uploadPage(Point pt, uint8_t* data, bgfx::ViewId blitViewId) +{ + // Get next staging texture to write to + auto writer = m_stagingPool.getTexture(); + m_stagingPool.next(); + + // Update texture with new atlas data + auto pagesize = uint16_t(m_info->GetPageSize()); + bgfx::updateTexture2D(writer, 0, 0, 0, 0, pagesize, pagesize, bgfx::copy(data, pagesize * pagesize * ChannelCount)); + + // Copy the texture part to the actual atlas texture + auto xpos = uint16_t(pt.m_x * pagesize); + auto ypos = uint16_t(pt.m_y * pagesize); + bgfx::blit(blitViewId, m_texture, 0, xpos, ypos, 0, writer, 0, 0, 0, 0, pagesize, pagesize); +} + +bgfx::TextureHandle TextureAtlas::getTexture() +{ + return m_texture; +} + +// FeedbackBuffer +FeedbackBuffer::FeedbackBuffer(VirtualTextureInfo* _info, int _width, int _height) + : m_info(_info) + , m_width(_width) + , m_height(_height) + , m_stagingPool(_width, _height, StagingTextureCount, true) +{ + // Setup classes + m_indexer = new PageIndexer(m_info); + m_requests.resize(m_indexer->getCount()); + // Initialize and clear buffers + m_downloadBuffer.resize(m_width * m_height * ChannelCount); + bx::memSet(&m_downloadBuffer[0], 0, m_width * m_height * ChannelCount); + clear(); + // Initialize feedback frame buffer + bgfx::TextureHandle feedbackFrameBufferTextures[] = + { + bgfx::createTexture2D(uint16_t(m_width), uint16_t(m_height), false, 1, bgfx::TextureFormat::BGRA8, BGFX_TEXTURE_RT), + bgfx::createTexture2D(uint16_t(m_width), uint16_t(m_height), false, 1, bgfx::TextureFormat::D24S8, BGFX_TEXTURE_RT), + }; + m_feedbackFrameBuffer = bgfx::createFrameBuffer(BX_COUNTOF(feedbackFrameBufferTextures), feedbackFrameBufferTextures, true); + m_lastStagingTexture = { bgfx::kInvalidHandle }; +} + +FeedbackBuffer::~FeedbackBuffer() +{ + delete m_indexer; + bgfx::destroy(m_feedbackFrameBuffer); +} + +void FeedbackBuffer::clear() +{ + // Clear Table + bx::memSet(&m_requests[0], 0, sizeof(int) * m_indexer->getCount()); +} + +void FeedbackBuffer::copy(bgfx::ViewId viewId) +{ + m_lastStagingTexture = m_stagingPool.getTexture(); + // Copy feedback buffer render target to staging texture + bgfx::blit(viewId, m_lastStagingTexture, 0, 0, bgfx::getTexture(m_feedbackFrameBuffer)); + m_stagingPool.next(); +} + +void FeedbackBuffer::download() +{ + // Check if there's an already rendered feedback buffer available + if (m_lastStagingTexture.idx == bgfx::kInvalidHandle) + { + return; + } + // Read the texture + bgfx::readTexture(m_lastStagingTexture, &m_downloadBuffer[0]); + // Loop through pixels and check if anything was written + auto data = &m_downloadBuffer[0]; + auto colors = (Color*)data; + auto dataSize = m_width * m_height; + for (int i = 0; i < dataSize; ++i) + { + auto& color = colors[i]; + if (color.m_a >= 0xff) + { + // Page found! Add it to the request queue + Page request = { color.m_b, color.m_g, color.m_r }; + addRequestAndParents(request); + // Clear the pixel, so that we don't have to do it in another pass + color = { 0,0,0,0 }; + } + } +} + +// This function validates the pages and adds the page's parents +// We do this so that we can fall back to them if we run out of memory +void FeedbackBuffer::addRequestAndParents(Page request) +{ + int PageTableSizeLog2 = (int)log2(m_info->GetPageTableSize()); + int count = PageTableSizeLog2 - request.m_mip + 1; + + for (int i = 0; i < count; ++i) + { + int xpos = request.m_x >> i; + int ypos = request.m_y >> i; + + Page page = { xpos, ypos, request.m_mip + i }; + + if (!m_indexer->isValid(page)) + { + return; + } + + ++m_requests[m_indexer->getIndexFromPage(page)]; + } +} + +const std::vector& FeedbackBuffer::getRequests() const +{ + return m_requests; +} + +bgfx::FrameBufferHandle FeedbackBuffer::getFrameBuffer() +{ + return m_feedbackFrameBuffer; +} + +int FeedbackBuffer::getWidth() const +{ + return m_width; +} + +int FeedbackBuffer::getHeight() const +{ + return m_height; +} + +// VirtualTexture +VirtualTexture::VirtualTexture(TileDataFile* _tileDataFile, VirtualTextureInfo* _info, int _atlassize, int _uploadsperframe, int _mipBias) + : m_tileDataFile(_tileDataFile) + , m_info(_info) + , m_uploadsPerFrame(_uploadsperframe) + , m_mipBias(_mipBias) + +{ + m_atlasCount = _atlassize / m_info->GetPageSize(); + + // Setup indexer + m_indexer = new PageIndexer(m_info); + m_pagesToLoad.reserve(m_indexer->getCount()); + + // Setup classes + m_atlas = new TextureAtlas(m_info, m_atlasCount, m_uploadsPerFrame); + m_loader = new PageLoader(m_tileDataFile, m_indexer, m_info); + m_cache = new PageCache(m_info, m_atlas, m_loader, m_indexer, m_atlasCount); + m_pageTable = new PageTable(m_cache, m_info, m_indexer); + + // Create uniforms + u_vt_settings_1 = bgfx::createUniform("u_vt_settings_1", bgfx::UniformType::Vec4); + u_vt_settings_2 = bgfx::createUniform("u_vt_settings_2", bgfx::UniformType::Vec4); + s_vt_page_table = bgfx::createUniform("s_vt_page_table", bgfx::UniformType::Int1); + s_vt_texture_atlas = bgfx::createUniform("s_vt_texture_atlas", bgfx::UniformType::Int1); +} + +VirtualTexture::~VirtualTexture() +{ + // Destroy + delete m_indexer; + delete m_atlas; + delete m_loader; + delete m_cache; + delete m_pageTable; + // Destroy all uniforms and textures + bgfx::destroy(u_vt_settings_1); + bgfx::destroy(u_vt_settings_2); + bgfx::destroy(s_vt_page_table); + bgfx::destroy(s_vt_texture_atlas); +} + +int VirtualTexture::getMipBias() const +{ + return m_mipBias; +} + +void VirtualTexture::setMipBias(int value) +{ + m_mipBias = value; + if (m_mipBias < 0) + m_mipBias = 0; +} + +void VirtualTexture::setUniforms() +{ + // Set uniforms + struct + { + struct + { + float VirtualTextureSize; + float ooAtlasScale; + float BorderScale; + float BorderOffset; + } m_settings_1; + struct + { + float MipBias; + float PageTableSize; + float unused1; + float unused2; + } m_settings_2; + } uniforms; + // Fill uniforms + int pagesize = m_info->GetPageSize(); + uniforms.m_settings_1.VirtualTextureSize = (float)m_info->m_virtualTextureSize; + uniforms.m_settings_1.ooAtlasScale = 1.0f / (float)m_atlasCount; + uniforms.m_settings_1.BorderScale = (float)((pagesize - 2.0f * m_info->m_borderSize) / pagesize); + uniforms.m_settings_1.BorderOffset = (float)m_info->m_borderSize / (float)pagesize; + uniforms.m_settings_2.MipBias = (float)m_mipBias; + uniforms.m_settings_2.PageTableSize = (float)m_info->GetPageTableSize(); + // Set uniforms + bgfx::setUniform(u_vt_settings_1, &uniforms.m_settings_1); + bgfx::setUniform(u_vt_settings_2, &uniforms.m_settings_2); + // Set textures + bgfx::setTexture(0, s_vt_page_table, m_pageTable->getTexture()); + bgfx::setTexture(1, s_vt_texture_atlas, m_atlas->getTexture()); +} + +void VirtualTexture::setUploadsPerFrame(int count) +{ + m_uploadsPerFrame = count; + m_atlas->setUploadsPerFrame(count); +} + +int VirtualTexture::getUploadsPerFrame() const +{ + return m_uploadsPerFrame; +} + + +void VirtualTexture::enableShowBoarders(bool enable) +{ + if (m_loader->m_showBorders == enable) + { + return; + } + m_loader->m_showBorders = enable; + clear(); +} + +bool VirtualTexture::isShowBoardersEnabled() const +{ + return m_loader->m_showBorders; +} + +void VirtualTexture::enableColorMipLevels(bool enable) +{ + if (m_loader->m_colorMipLevels == enable) + { + return; + } + m_loader->m_colorMipLevels = enable; + clear(); +} + +bool VirtualTexture::isColorMipLevelsEnabled() const +{ + return m_loader->m_colorMipLevels; +} + +bgfx::TextureHandle VirtualTexture::getAtlastTexture() +{ + return m_atlas->getTexture(); +} + +bgfx::TextureHandle VirtualTexture::getPageTableTexture() +{ + return m_pageTable->getTexture(); +} + +void VirtualTexture::clear() +{ + m_cache->clear(); +} + +void VirtualTexture::update(const std::vector& requests, bgfx::ViewId blitViewId) +{ + m_pagesToLoad.clear(); + + // Find out what is already in memory + // If it is, update it's position in the LRU collection + // Otherwise add it to the list of pages to load + int touched = 0; + for (int i = 0; i < requests.size(); ++i) + { + if (requests[i] > 0) + { + PageCount pc(m_indexer->getPageFromIndex(i), requests[i]); + if (!m_cache->touch(pc.m_page)) + { + m_pagesToLoad.push_back(pc); + } + else + { + ++touched; + } + } + } + + // Check to make sure we don't thrash + if (touched < m_atlasCount * m_atlasCount) + { + // sort by low res to high res and number of requests + std::sort(m_pagesToLoad.begin(), m_pagesToLoad.end()); + + // if more pages than will fit in memory or more than update per frame drop high res pages with lowest use count + int loadcount = bx::min(bx::min((int)m_pagesToLoad.size(), m_uploadsPerFrame), m_atlasCount * m_atlasCount); + for (int i = 0; i < loadcount; ++i) + m_cache->request(m_pagesToLoad[i].m_page, blitViewId); + } + else + { + // The problem here is that all pages in cache are requested and the new or high res ones don't get uploaded + // We can adjust the mip bias to make it all fit. This solves the problem of page cache thrashing + --m_mipBias; + } + + // Update the page table + m_pageTable->update(blitViewId); +} + +TileDataFile::TileDataFile(const std::string& filename, VirtualTextureInfo* _info, bool _readWrite) : m_info(_info) +{ + const char* access = _readWrite ? "w+b" : "rb"; + m_file = fopen(filename.c_str(), access); + m_size = m_info->GetPageSize() * m_info->GetPageSize() * ChannelCount; +} + +TileDataFile::~TileDataFile() +{ + fclose(m_file); +} + +void TileDataFile::readInfo() +{ + fseek(m_file, 0, SEEK_SET); + fread(m_info, sizeof(*m_info), 1, m_file); + m_size = m_info->GetPageSize() * m_info->GetPageSize() * ChannelCount; +} + +void TileDataFile::writeInfo() +{ + fseek(m_file, 0, SEEK_SET); + fwrite(m_info, sizeof(*m_info), 1, m_file); +} + +void TileDataFile::readPage(int index, uint8_t* data) +{ + fseek(m_file, m_size * index + TileFileDataOffset, SEEK_SET); + fread(data, m_size, 1, m_file); +} + +void TileDataFile::writePage(int index, uint8_t* data) +{ + fseek(m_file, m_size * index + TileFileDataOffset, SEEK_SET); + fwrite(data, m_size, 1, m_file); +} + +// TileGenerator +TileGenerator::TileGenerator(VirtualTextureInfo* _info) + : m_info(_info) + , m_indexer(nullptr) + , m_tileDataFile(nullptr) + , m_sourceImage(nullptr) + , m_page1Image(nullptr) + , m_page2Image(nullptr) + , m_2xtileImage(nullptr) + , m_4xtileImage(nullptr) + , m_tileImage(nullptr) +{ + m_tilesize = m_info->m_tileSize; + m_pagesize = m_info->GetPageSize(); +} + +TileGenerator::~TileGenerator() +{ + if (m_sourceImage != nullptr) + { + bimg::imageFree(m_sourceImage); + } + + delete m_indexer; + + delete m_page1Image; + delete m_page2Image; + delete m_2xtileImage; + delete m_4xtileImage; + delete m_tileImage; +} + +bool TileGenerator::generate(const std::string& filename) +{ + std::string cacheFilename = filename + ".cache"; + // Check if tile file already exist + { + bx::Error error; + bx::FileReader fileReader; + fileReader.open(bx::FilePath(cacheFilename.c_str()), &error); + if (error.isOk()) + { + bx::debugPrintf("Tile data file '%s' already exists. Skipping generation.\n", cacheFilename.c_str()); + return true; + } + } + // Read image + { + bx::debugPrintf("Reading image '%s'.\n", filename.c_str()); + bx::Error error; + bx::FileReader fileReader; + fileReader.open(bx::FilePath(filename.c_str()), &error); + if (!error.isOk()) + { + return false; + } + auto size = fileReader.seek(0, bx::Whence::End); + fileReader.seek(0, bx::Whence::Begin); + auto rawImage = (uint8_t*)BX_ALLOC(&m_allocator, size); + fileReader.read(rawImage, int32_t(size), &error); + fileReader.close(); + if (!error.isOk()) + { + return false; + } + m_sourceImage = bimg::imageParse(&m_allocator, rawImage, uint32_t(size), bimg::TextureFormat::BGRA8, &error); + BX_FREE(&m_allocator, rawImage); + if (!error.isOk()) + { + return false; + } + } + + // Setup + m_info->m_virtualTextureSize = int(m_sourceImage->m_width); + m_indexer = new PageIndexer(m_info); + + // Open tile data file + m_tileDataFile = new TileDataFile(cacheFilename, m_info, true); + + m_page1Image = new SimpleImage(m_pagesize, m_pagesize, ChannelCount, 0xff); + m_page2Image = new SimpleImage(m_pagesize, m_pagesize, ChannelCount, 0xff); + m_tileImage = new SimpleImage(m_tilesize, m_tilesize, ChannelCount, 0xff); + m_2xtileImage = new SimpleImage(m_tilesize * 2, m_tilesize * 2, ChannelCount, 0xff); + m_4xtileImage = new SimpleImage(m_tilesize * 4, m_tilesize * 4, ChannelCount, 0xff); + + // Generate tiles + bx::debugPrintf("Generating tiles\n"); + int mipcount = (int)log2(m_info->GetPageTableSize()); + for (int i = 0; i < mipcount + 1; ++i) + { + int count = (m_info->m_virtualTextureSize / m_tilesize) >> i; + bx::debugPrintf("Generating Mip:%d Count:%dx%d\n", i, count, count); + for (int y = 0; y < count; ++y) + { + for (int x = 0; x < count; ++x) + { + Page page = { x, y, i }; + int index = m_indexer->getIndexFromPage(page); + CopyTile(*m_page1Image, page); + m_tileDataFile->writePage(index, &m_page1Image->m_data[0]); + } + } + } + bx::debugPrintf("Finising\n"); + // Write header + m_tileDataFile->writeInfo(); + // Close tile file + delete m_tileDataFile; + m_tileDataFile = nullptr; + bx::debugPrintf("Done!\n"); + return true; +} + +void TileGenerator::CopyTile(SimpleImage& image, Page request) +{ + if (request.m_mip == 0) + { + int x = request.m_x * m_tilesize - m_info->m_borderSize; + int y = request.m_y * m_tilesize - m_info->m_borderSize; + // Copy sub-image with border + auto srcPitch = m_sourceImage->m_width * ChannelCount; + auto src = (uint8_t*)m_sourceImage->m_data; + auto dstPitch = image.m_width * image.m_channelCount; + auto dst = &image.m_data[0]; + for (int iy = 0; iy < m_pagesize; ++iy) + { + int ry = bx::clamp(y + iy, 0, (int)m_sourceImage->m_height - 1); + for (int ix = 0; ix < m_pagesize; ++ix) + { + int rx = bx::clamp(x + ix, 0, (int)m_sourceImage->m_width - 1); + bx::memCopy(&dst[iy * dstPitch + ix * image.m_channelCount], &src[ry * srcPitch + rx * ChannelCount], image.m_channelCount); + } + } + } + else + { + int xpos = request.m_x << 1; + int ypos = request.m_y << 1; + int mip = request.m_mip - 1; + + int size = m_info->GetPageTableSize() >> mip; + + m_4xtileImage->clear((uint8_t)request.m_mip); + + for (int y = 0; y < 4; ++y) + { + for (int x = 0; x < 4; ++x) + { + Page page = { xpos + x - 1, ypos + y - 1, mip }; + + // Wrap so we get the border sections of other pages + #define Modulus(_a_, _b_) (int)(_a_ - _b_ * floorf( (float)_a_ / (float)_b_)) + page.m_x = Modulus(page.m_x, size); + page.m_y = Modulus(page.m_y, size); + #undef Modulus + + m_tileDataFile->readPage(m_indexer->getIndexFromPage(page), &m_page2Image->m_data[0]); + + Rect src_rect = { m_info->m_borderSize, m_info->m_borderSize, m_tilesize, m_tilesize }; + Point dst_offset = { x * m_tilesize, y * m_tilesize }; + + m_4xtileImage->copy(dst_offset, *m_page2Image, src_rect); + } + } + + SimpleImage::mipmap(&m_4xtileImage->m_data[0], m_4xtileImage->m_width, ChannelCount, &m_2xtileImage->m_data[0]); + + Rect srect = { m_tilesize / 2 - m_info->m_borderSize, m_tilesize / 2 - m_info->m_borderSize, m_pagesize, m_pagesize }; + image.copy({ 0,0 }, *m_2xtileImage, srect); + } +} + +} // namespace vt diff --git a/examples/40-svt/vt.h b/examples/40-svt/vt.h new file mode 100644 index 000000000..3419f5125 --- /dev/null +++ b/examples/40-svt/vt.h @@ -0,0 +1,422 @@ +#pragma once +#include "common.h" +#include "bgfx_utils.h" +#include "bimg/decode.h" +#include +#include +#include + +namespace vt +{ + +// Forward declarations +class PageCache; +class TextureAtlas; +class TileDataFile; + +// Point +struct Point +{ + int m_x, m_y; +}; + +// Rect +struct Rect +{ + int minX() const + { + return m_x; + } + + int minY() const + { + return m_y; + } + + int maxX() const + { + return m_x + m_width; + } + + int maxY() const + { + return m_y + m_height; + } + + bool contains(const Point& p) const + { + return p.m_x >= minX() && p.m_y >= minY() && p.m_x < maxX() && p.m_y < maxY(); + } + + int m_x, m_y, m_width, m_height; +}; + +// Color +struct Color +{ + uint8_t m_r, m_g, m_b, m_a; +}; + +// Page +struct Page +{ + uint64_t hash() const; + bool operator==(const Page& page) const; + bool operator<(const Page& page) const; + + int m_x; + int m_y; + int m_mip; +}; + +// PageCount +struct PageCount +{ + Page m_page; + int m_count; + + PageCount(Page _page = Page(), int _count = 0); + + int compareTo(const PageCount& other) const; + bool operator==(const PageCount& other) const; + bool operator<(const PageCount& other) const; +}; + +// VirtualTextureInfo +struct VirtualTextureInfo +{ + VirtualTextureInfo(); + int GetPageSize() const; + int GetPageTableSize() const; + + int m_virtualTextureSize = 0; + int m_tileSize = 0; + int m_borderSize = 0; +}; + +// StagingPool +class StagingPool +{ +public: + StagingPool(int _width, int _height, int _count, bool _readBack); + ~StagingPool(); + + void grow(int count); + + bgfx::TextureHandle getTexture(); + void next(); + +private: + std::vector m_stagingTextures; + + int m_stagingTextureIndex; + int m_width; + int m_height; + uint64_t m_flags; +}; + +// PageIndexer +struct PageIndexer +{ +public: + PageIndexer(VirtualTextureInfo* _info); + + int getIndexFromPage(Page page); + Page getPageFromIndex(int index); + + bool isValid(Page page); + int getCount() const; + +private: + VirtualTextureInfo* m_info; + int m_mipcount; + std::vector m_offsets; // This stores the offsets to the first page of the start of a mipmap level + std::vector m_sizes; // This stores the sizes of various mip levels + std::vector m_reverse; + int m_count; +}; + +// SimpleImage +struct SimpleImage +{ + SimpleImage(int _width, int _height, int _channelCount, uint8_t _clearValue = 0); + SimpleImage(int _width, int _height, int _channelCount, std::vector& _data); + + void copy(Point dest_offset, SimpleImage& src, Rect src_rect); + void clear(uint8_t clearValue = 0); + void fill(Rect rect, uint8_t r, uint8_t g, uint8_t b, uint8_t a); + + static void mipmap(uint8_t* source, int size, int channels, uint8_t* dest); + + int m_width = 0; + int m_height = 0; + int m_channelCount = 0; + std::vector m_data; +}; + +// Quadtree +struct Quadtree +{ + Quadtree(Rect _rect, int _level); + ~Quadtree(); + + void add(Page request, Point mapping); + void remove(Page request); + void write(SimpleImage& image, int miplevel); + Rect getRectangle(int index); + + void write(Quadtree* node, SimpleImage& image, int miplevel); + static Quadtree* findPage(Quadtree* node, Page request, int* index); + + int m_level; + Rect m_rectangle; + Point m_mapping; + Quadtree* m_children[4]; +}; + +// PageTable +class PageTable +{ +public: + PageTable(PageCache* _cache, VirtualTextureInfo* _info, PageIndexer* _indexer); + ~PageTable(); + + void update(bgfx::ViewId blitViewId); + bgfx::TextureHandle getTexture(); + +private: + VirtualTextureInfo* m_info; + bgfx::TextureHandle m_texture; + PageIndexer* m_indexer; + Quadtree* m_quadtree; + bool m_quadtreeDirty; + + std::vector m_images; + std::vector m_stagingTextures; +}; + +// PageLoader +class PageLoader +{ +public: + struct ReadState + { + Page m_page; + std::vector m_data; + }; + + PageLoader(TileDataFile* _tileDataFile, PageIndexer* _indexer, VirtualTextureInfo* _info); + void submit(Page request); + void loadPage(ReadState& state); + void onPageLoadComplete(ReadState& state); + void copyBorder(uint8_t* image); + void copyColor(uint8_t* image, Page request); + + std::function loadComplete; + + bool m_colorMipLevels; + bool m_showBorders; + +private: + TileDataFile* m_tileDataFile; + PageIndexer* m_indexer; + VirtualTextureInfo* m_info; +}; + +// PageCache +class PageCache +{ +public: + PageCache(VirtualTextureInfo* _info, TextureAtlas* _atlas, PageLoader* _loader, PageIndexer* _indexer, int _count); + bool touch(Page page); + bool request(Page request, bgfx::ViewId blitViewId); + void clear(); + void loadComplete(Page page, uint8_t* data); + + // These callbacks are used to notify the other systems + std::function removed; + std::function added; + +private: + VirtualTextureInfo* m_info; + TextureAtlas* m_atlas; + PageLoader* m_loader; + PageIndexer* m_indexer; + + int m_count; + + struct LruPage + { + Page m_page; + Point m_point; + + bool operator==(const Page& other) const + { + return m_page == other; + } + }; + + int m_current; // This is used for generating the texture atlas indices before the lru is full + + std::set m_lru_used; + std::vector m_lru; + std::set m_loading; + + bgfx::ViewId m_blitViewId; +}; + +// TextureAtlas +class TextureAtlas +{ +public: + TextureAtlas(VirtualTextureInfo* _info, int count, int uploadsperframe); + ~TextureAtlas(); + + void setUploadsPerFrame(int count); + void uploadPage(Point pt, uint8_t* data, bgfx::ViewId blitViewId); + + bgfx::TextureHandle getTexture(); + +private: + VirtualTextureInfo* m_info; + bgfx::TextureHandle m_texture; + StagingPool m_stagingPool; +}; + +// FeedbackBuffer +class FeedbackBuffer +{ +public: + FeedbackBuffer(VirtualTextureInfo* _info, int _width, int _height); + ~FeedbackBuffer(); + + void clear(); + + void copy(bgfx::ViewId viewId); + void download(); + + // This function validates the pages and adds the page's parents + // We do this so that we can fall back to them if we run out of memory + void addRequestAndParents(Page request); + + const std::vector& getRequests() const; + bgfx::FrameBufferHandle getFrameBuffer(); + + int getWidth() const; + int getHeight() const; + +private: + VirtualTextureInfo* m_info; + PageIndexer* m_indexer; + + int m_width = 0; + int m_height = 0; + + StagingPool m_stagingPool; + bgfx::TextureHandle m_lastStagingTexture; + bgfx::FrameBufferHandle m_feedbackFrameBuffer; + + // This stores the pages by index. The int value is number of requests. + std::vector m_requests; + std::vector m_downloadBuffer; +}; + +// VirtualTexture +class VirtualTexture +{ +public: + VirtualTexture(TileDataFile* _tileDataFile, VirtualTextureInfo* _info, int _atlassize, int _uploadsperframe, int _mipBias = 4); + ~VirtualTexture(); + + int getMipBias() const; + void setMipBias(int value); + + void setUploadsPerFrame(int count); + int getUploadsPerFrame() const; + + void enableShowBoarders(bool enable); + bool isShowBoardersEnabled() const; + + void enableColorMipLevels(bool enable); + bool isColorMipLevelsEnabled() const; + + bgfx::TextureHandle getAtlastTexture(); + bgfx::TextureHandle getPageTableTexture(); + + void clear(); + void update(const std::vector& requests, bgfx::ViewId blitViewId); + + void setUniforms(); + +private: + TileDataFile* m_tileDataFile; + VirtualTextureInfo* m_info; + PageIndexer* m_indexer; + PageTable* m_pageTable; + TextureAtlas* m_atlas; + PageLoader* m_loader; + PageCache* m_cache; + + int m_atlasCount; + int m_uploadsPerFrame; + + std::vector m_pagesToLoad; + + int m_mipBias; + + bgfx::UniformHandle u_vt_settings_1; + bgfx::UniformHandle u_vt_settings_2; + bgfx::UniformHandle s_vt_page_table; + bgfx::UniformHandle s_vt_texture_atlas; +}; + +// TileDataFile +class TileDataFile +{ +public: + TileDataFile(const std::string& filename, VirtualTextureInfo* _info, bool _readWrite = false); + ~TileDataFile(); + + void readInfo(); + void writeInfo(); + + void readPage(int index, uint8_t* data); + void writePage(int index, uint8_t* data); + +private: + VirtualTextureInfo* m_info; + int m_size; + FILE* m_file; +}; + +// TileGenerator +class TileGenerator +{ +public: + TileGenerator(VirtualTextureInfo* _info); + ~TileGenerator(); + + bool generate(const std::string& filename); + +private: + void CopyTile(SimpleImage& image, Page request); + +private: + VirtualTextureInfo* m_info; + PageIndexer* m_indexer; + TileDataFile* m_tileDataFile; + + int m_tilesize; + int m_pagesize; + + bx::DefaultAllocator m_allocator; + bimg::ImageContainer* m_sourceImage; + + SimpleImage* m_page1Image; + SimpleImage* m_page2Image; + SimpleImage* m_2xtileImage; + SimpleImage* m_4xtileImage; + SimpleImage* m_tileImage; +}; + +} // namespace vt diff --git a/examples/runtime/shaders/dx11/fs_vt_mip.bin b/examples/runtime/shaders/dx11/fs_vt_mip.bin new file mode 100644 index 000000000..55acc64ab Binary files /dev/null and b/examples/runtime/shaders/dx11/fs_vt_mip.bin differ diff --git a/examples/runtime/shaders/dx11/fs_vt_unlit.bin b/examples/runtime/shaders/dx11/fs_vt_unlit.bin new file mode 100644 index 000000000..e153c17d9 Binary files /dev/null and b/examples/runtime/shaders/dx11/fs_vt_unlit.bin differ diff --git a/examples/runtime/shaders/dx11/vs_vt_generic.bin b/examples/runtime/shaders/dx11/vs_vt_generic.bin new file mode 100644 index 000000000..71e42b378 Binary files /dev/null and b/examples/runtime/shaders/dx11/vs_vt_generic.bin differ diff --git a/examples/runtime/shaders/glsl/fs_vt_mip.bin b/examples/runtime/shaders/glsl/fs_vt_mip.bin new file mode 100644 index 000000000..1ccac963c Binary files /dev/null and b/examples/runtime/shaders/glsl/fs_vt_mip.bin differ diff --git a/examples/runtime/shaders/glsl/fs_vt_unlit.bin b/examples/runtime/shaders/glsl/fs_vt_unlit.bin new file mode 100644 index 000000000..533063688 Binary files /dev/null and b/examples/runtime/shaders/glsl/fs_vt_unlit.bin differ diff --git a/examples/runtime/shaders/glsl/vs_vt_generic.bin b/examples/runtime/shaders/glsl/vs_vt_generic.bin new file mode 100644 index 000000000..ce72970d9 Binary files /dev/null and b/examples/runtime/shaders/glsl/vs_vt_generic.bin differ diff --git a/examples/runtime/textures/8k_mars.jpg b/examples/runtime/textures/8k_mars.jpg new file mode 100644 index 000000000..fa0feb693 Binary files /dev/null and b/examples/runtime/textures/8k_mars.jpg differ diff --git a/scripts/genie.lua b/scripts/genie.lua index c5728f006..0e119a071 100644 --- a/scripts/genie.lua +++ b/scripts/genie.lua @@ -449,6 +449,7 @@ or _OPTIONS["with-combined-examples"] then , "37-gpudrivenrendering" , "38-bloom" , "39-assao" + , "40-svt" ) -- C99 source doesn't compile under WinRT settings