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); }