From 781afaa8524e8261c33009bbb531ca6adeed2269 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Sat, 29 Aug 2020 00:16:21 -0700 Subject: [PATCH] [wpigui] Refactor texture handling The platform-specific code now only has create, update, and delete texture. Image reading functions have been moved to common code. Also add pixel data functions and image data functions in addition to image file loading. --- .../src/main/native/cpp/Field2D.cpp | 35 ++- .../src/main/native/cpp/HALSimGui.cpp | 5 +- wpigui/src/main/native/cpp/wpigui.cpp | 61 ++++- .../native/directx11/wpigui_directx11.cpp | 58 +++-- wpigui/src/main/native/include/wpigui.h | 209 +++++++++++++++++- wpigui/src/main/native/metal/wpigui_metal.mm | 42 ++-- .../main/native/opengl3/wpigui_opengl3.cpp | 49 ++-- 7 files changed, 391 insertions(+), 68 deletions(-) diff --git a/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp b/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp index ea3f7fd491..0ed2599a0b 100644 --- a/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/Field2D.cpp @@ -69,7 +69,7 @@ class FieldInfo { bool LoadImageImpl(const char* fn); std::string m_filename; - ImTextureID m_texture = 0; + gui::Texture m_texture; int m_imageWidth = 0; int m_imageHeight = 0; int m_top = 0; @@ -119,7 +119,7 @@ class RobotInfo { bool LoadImageImpl(const char* fn); std::string m_filename; - ImTextureID m_texture = 0; + gui::Texture m_texture; HAL_SimDeviceHandle m_devHandle = 0; hal::SimDouble m_xHandle; @@ -166,8 +166,7 @@ static void Field2DWriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, } void FieldInfo::Reset() { - if (m_texture != 0) gui::DeleteTexture(m_texture); - m_texture = 0; + m_texture = gui::Texture{}; m_filename.clear(); m_imageWidth = 0; m_imageHeight = 0; @@ -193,7 +192,7 @@ void FieldInfo::LoadImage() { } m_fileOpener.reset(); } - if (m_texture == 0 && !m_filename.empty()) { + if (!m_texture && !m_filename.empty()) { if (!LoadImageImpl(m_filename.c_str())) m_filename.clear(); } } @@ -290,13 +289,14 @@ void FieldInfo::LoadJson(const wpi::Twine& jsonfile) { bool FieldInfo::LoadImageImpl(const char* fn) { wpi::outs() << "GUI: loading field image '" << fn << "'\n"; - auto oldTexture = m_texture; - if (!gui::LoadTextureFromFile(fn, &m_texture, &m_imageWidth, - &m_imageHeight)) { + auto texture = gui::Texture::CreateFromFile(fn); + if (!texture) { wpi::errs() << "GUI: could not read field image\n"; return false; } - if (oldTexture != 0) gui::DeleteTexture(oldTexture); + m_texture = std::move(texture); + m_imageWidth = m_texture.GetWidth(); + m_imageHeight = m_texture.GetHeight(); m_filename = fn; return true; } @@ -309,7 +309,7 @@ FieldFrameData FieldInfo::GetFrameData() const { ffd.imageMax = ImGui::GetWindowContentRegionMax(); // fit the image into the window - if (m_texture != 0 && m_imageHeight != 0 && m_imageWidth != 0) + if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) MaxFit(&ffd.imageMin, &ffd.imageMax, m_imageWidth, m_imageHeight); ImVec2 min = ffd.imageMin; @@ -334,7 +334,7 @@ FieldFrameData FieldInfo::GetFrameData() const { void FieldInfo::Draw(ImDrawList* drawList, const ImVec2& windowPos, const FieldFrameData& ffd) const { - if (m_texture != 0 && m_imageHeight != 0 && m_imageWidth != 0) { + if (m_texture && m_imageHeight != 0 && m_imageWidth != 0) { drawList->AddImage(m_texture, windowPos + ffd.imageMin, windowPos + ffd.imageMax); } @@ -381,8 +381,7 @@ void FieldInfo::WriteIni(ImGuiTextBuffer* out) const { } void RobotInfo::Reset() { - if (m_texture != 0) gui::DeleteTexture(m_texture); - m_texture = 0; + m_texture = gui::Texture{}; m_filename.clear(); } @@ -392,19 +391,19 @@ void RobotInfo::LoadImage() { if (!result.empty()) LoadImageImpl(result[0].c_str()); m_fileOpener.reset(); } - if (m_texture == 0 && !m_filename.empty()) { + if (!m_texture && !m_filename.empty()) { if (!LoadImageImpl(m_filename.c_str())) m_filename.clear(); } } bool RobotInfo::LoadImageImpl(const char* fn) { wpi::outs() << "GUI: loading robot image '" << fn << "'\n"; - auto oldTexture = m_texture; - if (!gui::LoadTextureFromFile(fn, &m_texture, nullptr, nullptr)) { + auto texture = gui::Texture::CreateFromFile(fn); + if (!texture) { wpi::errs() << "GUI: could not read robot image\n"; return false; } - if (oldTexture != 0) gui::DeleteTexture(oldTexture); + m_texture = std::move(texture); m_filename = fn; return true; } @@ -470,7 +469,7 @@ RobotFrameData RobotInfo::GetFrameData(const FieldFrameData& ffd) const { void RobotInfo::Draw(ImDrawList* drawList, const ImVec2& windowPos, const RobotFrameData& rfd, int hit, float hitRadius) const { - if (m_texture != 0) { + if (m_texture) { drawList->AddImageQuad( m_texture, windowPos + rfd.corners[0], windowPos + rfd.corners[1], windowPos + rfd.corners[2], windowPos + rfd.corners[3]); diff --git a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp index 4d769cc199..98034943d1 100644 --- a/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp +++ b/simulation/halsim_gui/src/main/native/cpp/HALSimGui.cpp @@ -277,7 +277,10 @@ bool HALSimGui::Initialize() { return true; } -void HALSimGui::Main(void*) { gui::Main(); } +void HALSimGui::Main(void*) { + gui::Main(); + gui::DestroyContext(); +} void HALSimGui::Exit(void*) { gui::Exit(); } diff --git a/wpigui/src/main/native/cpp/wpigui.cpp b/wpigui/src/main/native/cpp/wpigui.cpp index eac88ac592..492726b808 100644 --- a/wpigui/src/main/native/cpp/wpigui.cpp +++ b/wpigui/src/main/native/cpp/wpigui.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "wpigui_internal.h" @@ -271,7 +272,10 @@ void gui::CommonRenderFrame() { ImGui::Render(); } -void gui::Exit() { gContext->exit = true; } +void gui::Exit() { + if (!gContext) return; + gContext->exit = true; +} void gui::AddInit(std::function initialize) { if (initialize) gContext->initializers.emplace_back(std::move(initialize)); @@ -354,4 +358,59 @@ void gui::EmitViewMenu() { } } +bool gui::UpdateTextureFromImage(ImTextureID* texture, int width, int height, + const unsigned char* data, int len) { + // Load from memory + int width2 = 0; + int height2 = 0; + unsigned char* imgData = + stbi_load_from_memory(data, len, &width2, &height2, nullptr, 4); + if (!data) return false; + + if (width2 == width && height2 == height) + UpdateTexture(texture, kPixelRGBA, width2, height2, imgData); + else + *texture = CreateTexture(kPixelRGBA, width2, height2, imgData); + + stbi_image_free(imgData); + + return true; +} + +bool gui::CreateTextureFromFile(const char* filename, ImTextureID* out_texture, + int* out_width, int* out_height) { + // Load from file + int width = 0; + int height = 0; + unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4); + if (!data) return false; + + *out_texture = CreateTexture(kPixelRGBA, width, height, data); + if (out_width) *out_width = width; + if (out_height) *out_height = height; + + stbi_image_free(data); + + return true; +} + +bool gui::CreateTextureFromImage(const unsigned char* data, int len, + ImTextureID* out_texture, int* out_width, + int* out_height) { + // Load from memory + int width = 0; + int height = 0; + unsigned char* imgData = + stbi_load_from_memory(data, len, &width, &height, nullptr, 4); + if (!imgData) return false; + + *out_texture = CreateTexture(kPixelRGBA, width, height, imgData); + if (out_width) *out_width = width; + if (out_height) *out_height = height; + + stbi_image_free(imgData); + + return true; +} + } // namespace wpi diff --git a/wpigui/src/main/native/directx11/wpigui_directx11.cpp b/wpigui/src/main/native/directx11/wpigui_directx11.cpp index 4fbf0379c5..efd5865c95 100644 --- a/wpigui/src/main/native/directx11/wpigui_directx11.cpp +++ b/wpigui/src/main/native/directx11/wpigui_directx11.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include "wpigui.h" #include "wpigui_internal.h" @@ -32,6 +31,7 @@ struct PlatformContext { } // namespace static PlatformContext* gPlatformContext; +static bool gPlatformValid = false; static void CreateRenderTarget() { ID3D11Texture2D* pBackBuffer; @@ -127,6 +127,7 @@ bool gui::PlatformInitRenderer() { ImGui_ImplDX11_Init(gPlatformContext->pd3dDevice, gPlatformContext->pd3dDeviceContext); + gPlatformValid = true; return true; } @@ -146,15 +147,25 @@ void gui::PlatformRenderFrame() { // gPlatformContext->pSwapChain->Present(0, 0); // Present without vsync } -void gui::PlatformShutdown() { ImGui_ImplDX11_Shutdown(); } +void gui::PlatformShutdown() { + gPlatformValid = false; + ImGui_ImplDX11_Shutdown(); +} -bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture, - int* out_width, int* out_height) { - // Load from disk into a raw RGBA buffer - int width = 0; - int height = 0; - unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4); - if (!data) return false; +static inline DXGI_FORMAT DXPixelFormat(PixelFormat format) { + switch (format) { + case kPixelRGBA: + return DXGI_FORMAT_R8G8B8A8_UNORM; + case kPixelBGRA: + return DXGI_FORMAT_B8G8R8A8_UNORM; + default: + return DXGI_FORMAT_R8G8B8A8_UNORM; + } +} + +ImTextureID gui::CreateTexture(PixelFormat format, int width, int height, + const unsigned char* data) { + if (!gPlatformValid) return nullptr; // Create texture D3D11_TEXTURE2D_DESC desc; @@ -188,15 +199,34 @@ bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture, &srv); pTexture->Release(); - *out_texture = srv; - *out_width = width; - *out_height = height; - stbi_image_free(data); + return srv; +} - return true; +void gui::UpdateTexture(ImTextureID texture, PixelFormat, int width, int height, + const unsigned char* data) { + if (!texture) return; + + D3D11_BOX box; + box.front = 0; + box.back = 1; + box.left = 0; + box.right = width; + box.top = 0; + box.bottom = height; + + ID3D11Resource* resource = nullptr; + static_cast(texture)->GetResource(&resource); + + if (resource) { + gPlatformContext->pd3dDeviceContext->UpdateSubresource( + resource, 0, &box, data, width * 4, width * height * 4); + + resource->Release(); + } } void gui::DeleteTexture(ImTextureID texture) { + if (!gPlatformValid) return; if (texture) static_cast(texture)->Release(); } diff --git a/wpigui/src/main/native/include/wpigui.h b/wpigui/src/main/native/include/wpigui.h index a6b0a4f4be..f4cecbbecb 100644 --- a/wpigui/src/main/native/include/wpigui.h +++ b/wpigui/src/main/native/include/wpigui.h @@ -130,7 +130,56 @@ void SetClearColor(ImVec4 color); void EmitViewMenu(); /** - * Loads a texture from a file. + * Pixel formats for texture pixel data. + */ +enum PixelFormat { kPixelRGBA, kPixelBGRA }; + +/** + * Creates a texture from pixel data. + * + * @param format pixel format + * @param width image width + * @param height image height + * @param data pixel data + * @return Texture + */ +ImTextureID CreateTexture(PixelFormat format, int width, int height, + const unsigned char* data); + +/** + * Updates a texture from pixel data. + * The passed-in width and height must match the width and height of the + * texture. + * + * @param texture texture + * @param format pixel format + * @param width texture width + * @param height texture height + * @param data pixel data + */ +void UpdateTexture(ImTextureID texture, PixelFormat format, int width, + int height, const unsigned char* data); + +/** + * Updates a texture from image data. + * The pixel format of the texture must be RGBA. The passed-in width and + * height must match the width and height of the texture. If the width and + * height of the image differ from the passed-in width and height, a new + * texture is created (note this may be inefficient). + * + * @param texture texture (pointer, may be updated) + * @param width texture width + * @param height texture height + * @param data image data + * @param len image data length + * + * @return True on success, false on failure. + */ +bool UpdateTextureFromImage(ImTextureID* texture, int width, int height, + const unsigned char* data, int len); + +/** + * Creates a texture from an image file. * * @param filename filename * @param out_texture texture (output) @@ -138,8 +187,22 @@ void EmitViewMenu(); * @param out_height image height (output) * @return True on success, false on failure. */ -bool LoadTextureFromFile(const char* filename, ImTextureID* out_texture, - int* out_width, int* out_height); +bool CreateTextureFromFile(const char* filename, ImTextureID* out_texture, + int* out_width, int* out_height); + +/** + * Creates a texture from image data. + * + * @param data image data + * @param len image data length + * @param out_texture texture (output) + * @param out_width image width (output) + * @param out_height image height (output) + * @return True on success, false on failure. + */ +bool CreateTextureFromImage(const unsigned char* data, int len, + ImTextureID* out_texture, int* out_width, + int* out_height); /** * Deletes a texture. @@ -148,4 +211,144 @@ bool LoadTextureFromFile(const char* filename, ImTextureID* out_texture, */ void DeleteTexture(ImTextureID texture); +/** + * RAII wrapper around ImTextureID. Also keeps track of width, height, and + * pixel format. + */ +class Texture { + public: + Texture() = default; + + /** + * Constructs a texture from pixel data. + * + * @param format pixel format + * @param width image width + * @param height image height + * @param data pixel data + */ + Texture(PixelFormat format, int width, int height, const unsigned char* data) + : m_format{format}, m_width{width}, m_height{height} { + m_texture = CreateTexture(format, width, height, data); + } + + Texture(const Texture&) = delete; + Texture(Texture&& oth) + : m_texture{oth.m_texture}, + m_format{oth.m_format}, + m_width{oth.m_width}, + m_height{oth.m_height} { + oth.m_texture = 0; + } + + Texture& operator=(const Texture&) = delete; + Texture& operator=(Texture&& oth) { + if (m_texture) DeleteTexture(m_texture); + m_texture = oth.m_texture; + oth.m_texture = 0; + m_format = oth.m_format; + m_width = oth.m_width; + m_height = oth.m_height; + return *this; + } + + ~Texture() { + if (m_texture) DeleteTexture(m_texture); + } + + /** + * Evaluates to true if the texture is valid. + */ + explicit operator bool() const { return m_texture; } + + /** + * Implicit conversion to ImTextureID. + */ + operator ImTextureID() const { return m_texture; } + + /** + * Gets the texture pixel format. + * + * @return pixel format + */ + PixelFormat GetFormat() const { return m_format; } + + /** + * Gets the texture width. + * + * @return width + */ + int GetWidth() const { return m_width; } + + /** + * Gets the texture height. + * + * @return height + */ + int GetHeight() const { return m_height; } + + /** + * Updates the texture from pixel data. + * The image data size and format is assumed to match that of the texture. + * + * @param format pixel format + * @param data pixel data + */ + void Update(const unsigned char* data) { + UpdateTexture(m_texture, m_format, m_width, m_height, data); + } + + /** + * Updates the texture from image data. + * The pixel format of the texture must be RGBA. If the width and height of + * the image differ from the texture width and height, a new texture is + * created (note this may be inefficient). + * + * @param data image data + * @param len image data length + * + * @return True on success, false on failure. + */ + bool UpdateFromImage(const unsigned char* data, int len) { + return UpdateTextureFromImage(&m_texture, m_width, m_height, data, len); + } + + /** + * Creates a texture by loading an image file. + * + * @param filename filename + * + * @return Texture, or invalid (empty) texture on failure. + */ + static Texture CreateFromFile(const char* filename) { + Texture texture; + if (!CreateTextureFromFile(filename, &texture.m_texture, &texture.m_width, + &texture.m_height)) + return {}; + return texture; + } + + /** + * Creates a texture from image data. + * + * @param data image data + * @param len image data length + * + * @return Texture, or invalid (empty) texture on failure. + */ + static Texture CreateFromImage(const unsigned char* data, int len) { + Texture texture; + if (!CreateTextureFromImage(data, len, &texture.m_texture, &texture.m_width, + &texture.m_height)) + return {}; + return texture; + } + + private: + ImTextureID m_texture = nullptr; + PixelFormat m_format = kPixelRGBA; + int m_width = 0; + int m_height = 0; +}; + } // namespace wpi::gui diff --git a/wpigui/src/main/native/metal/wpigui_metal.mm b/wpigui/src/main/native/metal/wpigui_metal.mm index d60e75daf9..fb41dbc202 100644 --- a/wpigui/src/main/native/metal/wpigui_metal.mm +++ b/wpigui/src/main/native/metal/wpigui_metal.mm @@ -16,7 +16,6 @@ #include #include #include -#include #include "wpigui.h" #include "wpigui_internal.h" @@ -32,6 +31,7 @@ struct PlatformContext { } // namespace static PlatformContext* gPlatformContext; +static bool gPlatformValid = false; namespace wpi { @@ -66,6 +66,7 @@ bool gui::PlatformInitRenderer() { gPlatformContext->renderPassDescriptor = [MTLRenderPassDescriptor new]; + gPlatformValid = true; return true; } @@ -103,17 +104,26 @@ void gui::PlatformRenderFrame() { void gui::PlatformShutdown() { ImGui_ImplMetal_Shutdown(); + gPlatformValid = false; } -bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture, - int* out_width, int* out_height) { - // Load from file - int width = 0; - int height = 0; - unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4); - if (!data) return false; +static inline MTLPixelFormat MetalPixelFormat(PixelFormat format) { + switch (format) { + case kPixelRGBA: + return MTLPixelFormatRGBA8Unorm; + case kPixelBGRA: + return MTLPixelFormatBGRA8Unorm; + default: + return MTLPixelFormatRGBA8Unorm; + } +} - MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:width height:height mipmapped:NO]; +ImTextureID gui::CreateTexture(PixelFormat format, int width, int height, + const unsigned char* data) { + if (!gPlatformValid) return nullptr; + + MTLPixelFormat fmt = MetalPixelFormat(format); + MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:fmt width:width height:height mipmapped:NO]; textureDescriptor.usage = MTLTextureUsageShaderRead; #if TARGET_OS_OSX textureDescriptor.storageMode = MTLStorageModeManaged; @@ -123,16 +133,18 @@ bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture, id texture = [gPlatformContext->layer.device newTextureWithDescriptor:textureDescriptor]; [texture replaceRegion:MTLRegionMake2D(0, 0, width, height) mipmapLevel:0 withBytes:data bytesPerRow:width * 4]; - *out_texture = (__bridge_retained void *)texture; - *out_width = width; - *out_height = height; - stbi_image_free(data); + return (__bridge_retained void *)texture; +} - return true; +void gui::UpdateTexture(ImTextureID texture, PixelFormat, int width, + int height, const unsigned char* data) { + if (!texture) return; + id mtlTexture = (__bridge id )texture; + [mtlTexture replaceRegion:MTLRegionMake2D(0, 0, width, height) mipmapLevel:0 withBytes:data bytesPerRow:width * 4]; } void gui::DeleteTexture(ImTextureID texture) { - if (!texture) return; + if (!gPlatformValid || !texture) return; id mtlTexture = (__bridge_transfer id )texture; (void)mtlTexture; } diff --git a/wpigui/src/main/native/opengl3/wpigui_opengl3.cpp b/wpigui/src/main/native/opengl3/wpigui_opengl3.cpp index a216f60bf2..f6837d6c3f 100644 --- a/wpigui/src/main/native/opengl3/wpigui_opengl3.cpp +++ b/wpigui/src/main/native/opengl3/wpigui_opengl3.cpp @@ -12,13 +12,14 @@ #include #include #include -#include #include "wpigui.h" #include "wpigui_internal.h" using namespace wpi::gui; +static bool gPlatformValid = false; + namespace wpi { void gui::PlatformCreateContext() {} @@ -73,6 +74,7 @@ bool gui::PlatformInitRenderer() { #endif ImGui_ImplOpenGL3_Init(glsl_version); + gPlatformValid = true; return true; } @@ -92,15 +94,25 @@ void gui::PlatformRenderFrame() { glfwSwapBuffers(gContext->window); } -void gui::PlatformShutdown() { ImGui_ImplOpenGL3_Shutdown(); } +void gui::PlatformShutdown() { + gPlatformValid = false; + ImGui_ImplOpenGL3_Shutdown(); +} -bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture, - int* out_width, int* out_height) { - // Load from file - int width = 0; - int height = 0; - unsigned char* data = stbi_load(filename, &width, &height, nullptr, 4); - if (!data) return false; +static inline GLenum GLPixelFormat(PixelFormat format) { + switch (format) { + case kPixelRGBA: + return GL_RGBA; + case kPixelBGRA: + return GL_BGRA; + default: + return GL_RGBA; + } +} + +ImTextureID gui::CreateTexture(PixelFormat format, int width, int height, + const unsigned char* data) { + if (!gPlatformValid) return nullptr; // Create a OpenGL texture identifier GLuint texture; @@ -113,18 +125,23 @@ bool gui::LoadTextureFromFile(const char* filename, ImTextureID* out_texture, // Upload pixels into texture glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, - GL_UNSIGNED_BYTE, data); - stbi_image_free(data); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, + GLPixelFormat(format), GL_UNSIGNED_BYTE, data); - *out_texture = reinterpret_cast(static_cast(texture)); - if (out_width) *out_width = width; - if (out_height) *out_height = height; + return reinterpret_cast(static_cast(texture)); +} - return true; +void gui::UpdateTexture(ImTextureID texture, PixelFormat format, int width, + int height, const unsigned char* data) { + GLuint glTexture = static_cast(reinterpret_cast(texture)); + if (glTexture == 0) return; + glBindTexture(GL_TEXTURE_2D, glTexture); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GLPixelFormat(format), + GL_UNSIGNED_BYTE, data); } void gui::DeleteTexture(ImTextureID texture) { + if (!gPlatformValid) return; GLuint glTexture = static_cast(reinterpret_cast(texture)); if (glTexture != 0) glDeleteTextures(1, &glTexture); }