# HG changeset patch # User Sebastien Jodogne # Date 1680189626 -7200 # Node ID 0d814292a17ec8dbaca16375ff9633673e0b3b19 # Parent 85ab86f10d014a33a4e9f5d22a6ecb59b54c07f9# Parent d77fea5934fbb57ed8f277215c417a8f0cdfdc1b integration mainline->deep-learning diff -r d77fea5934fb -r 0d814292a17e Applications/Samples/WebAssembly/CMakeLists.txt --- a/Applications/Samples/WebAssembly/CMakeLists.txt Thu Mar 30 17:20:01 2023 +0200 +++ b/Applications/Samples/WebAssembly/CMakeLists.txt Thu Mar 30 17:20:26 2023 +0200 @@ -45,12 +45,14 @@ set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1") endif() +set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ENVIRONMENT=web") set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'") set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1") set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=268435456") # 256MB + resize set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1") add_definitions( -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 + -DORTHANC_WEBGL2_HEAP_COMPAT=0 ) diff -r d77fea5934fb -r 0d814292a17e Applications/StoneWebViewer/WebApplication/app.js --- a/Applications/StoneWebViewer/WebApplication/app.js Thu Mar 30 17:20:01 2023 +0200 +++ b/Applications/StoneWebViewer/WebApplication/app.js Thu Mar 30 17:20:26 2023 +0200 @@ -603,7 +603,11 @@ series: [], studies: [], seriesIndex: {}, // Maps "SeriesInstanceUID" to "index in this.series" - virtualSeriesThumbnails: {} + virtualSeriesThumbnails: {}, + + deepLearningReady: false, + deepLearningProgress: 0, // Floating-point number in the range [0..1] + deepLearningStartTime: null } }, computed: { @@ -1301,6 +1305,11 @@ }); }, + ApplyDeepLearning: function() { + stone.ApplyDeepLearningModel(this.GetActiveCanvas()); + app.deepLearningStartTime = performance.now(); + }, + ChangeActiveSeries: function(offset) { var seriesTags = this.GetActiveViewportSeriesTags(); if (seriesTags !== null) { @@ -1726,3 +1735,18 @@ } } }); + + +window.addEventListener('DeepLearningInitialized', function() { + stone.LoadDeepLearningModel('model.message'); +}); + +window.addEventListener('DeepLearningModelReady', function() { + app.deepLearningReady = true; + app.deepLearningProgress = 0; +}); + +window.addEventListener('DeepLearningStep', function(args) { + app.deepLearningProgress = args.detail.progress; + console.log('Elapsed time: ' + Math.round(performance.now() - app.deepLearningStartTime) + 'ms'); +}); diff -r d77fea5934fb -r 0d814292a17e Applications/StoneWebViewer/WebApplication/index.html --- a/Applications/StoneWebViewer/WebApplication/index.html Thu Mar 30 17:20:01 2023 +0200 +++ b/Applications/StoneWebViewer/WebApplication/index.html Thu Mar 30 17:20:26 2023 +0200 @@ -316,11 +316,23 @@
+
+
+ +
+ +
+
+
+
+
+
+
-
+
stoneAnnotations_; - + bool linearInterpolation_; + boost::shared_ptr deepLearningMask_; + std::string deepLearningSopInstanceUid_; + unsigned int deepLearningFrameNumber_; void ScheduleNextPrefetch() { @@ -2246,6 +2250,26 @@ } } + std::unique_ptr deepLearningLayer; + + if (deepLearningMask_.get() != NULL && + deepLearningSopInstanceUid_ == instance.GetSopInstanceUid() && + deepLearningFrameNumber_ == frameIndex) + { + std::vector lut(4 * 256); + for (unsigned int v = 128; v < 256; v++) + { + lut[4 * v] = 196; + lut[4 * v + 1] = 0; + lut[4 * v + 2] = 0; + lut[4 * v + 3] = 196; + } + + deepLearningLayer.reset(new OrthancStone::LookupTableTextureSceneLayer(*deepLearningMask_)); + deepLearningLayer->SetLookupTable(lut); + deepLearningLayer->SetPixelSpacing(pixelSpacingX, pixelSpacingY); + } + StoneAnnotationsRegistry::GetInstance().Load(*stoneAnnotations_, instance.GetSopInstanceUid(), frameIndex); // Orientation markers, new in Stone Web viewer 2.4 @@ -2322,6 +2346,15 @@ scene.DeleteLayer(LAYER_ORIENTATION_MARKERS); } + if (deepLearningLayer.get() != NULL) + { + scene.SetLayer(LAYER_DEEP_LEARNING, deepLearningLayer.release()); + } + else + { + scene.DeleteLayer(LAYER_DEEP_LEARNING); + } + stoneAnnotations_->Render(scene); // Necessary for "FitContent()" to work if (fitNextContent_) @@ -2348,7 +2381,7 @@ { const size_t cursorIndex = cursor_->GetCurrentIndex(); const OrthancStone::DicomInstanceParameters& instance = frames_->GetInstanceOfFrame(cursorIndex); - const size_t frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); + const unsigned int frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); // Only change the scene if the loaded frame still corresponds to the current cursor if (instance.GetSopInstanceUid() == loadedSopInstanceUid && @@ -2654,7 +2687,7 @@ { const size_t cursorIndex = cursor_->GetCurrentIndex(); const OrthancStone::DicomInstanceParameters& instance = frames_->GetInstanceOfFrame(cursorIndex); - const size_t frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); + const unsigned int frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); StoneAnnotationsRegistry::GetInstance().Save(instance.GetSopInstanceUid(), frameNumber, *stoneAnnotations_); @@ -2880,7 +2913,7 @@ const size_t cursorIndex = cursor_->GetCurrentIndex(); const OrthancStone::DicomInstanceParameters& instance = frames_->GetInstanceOfFrame(cursorIndex); - const size_t frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); + const unsigned int frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); FramesCache::Accessor accessor(*framesCache_, instance.GetSopInstanceUid(), frameNumber); if (accessor.IsValid()) @@ -3454,7 +3487,7 @@ { const size_t cursorIndex = cursor_->GetCurrentIndex(); const OrthancStone::DicomInstanceParameters& instance = frames_->GetInstanceOfFrame(cursorIndex); - const size_t frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); + const unsigned int frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); if (instance.GetSopInstanceUid() == sopInstanceUid && frameNumber == frame) @@ -3470,6 +3503,7 @@ } } + void SetLinearInterpolation(bool linearInterpolation) { if (linearInterpolation_ != linearInterpolation) @@ -3479,6 +3513,7 @@ } } + void AddTextAnnotation(const std::string& label, const OrthancStone::ScenePoint2D& pointedPosition, const OrthancStone::ScenePoint2D& labelPosition) @@ -3488,6 +3523,43 @@ } + bool GetCurrentFrame(std::string& sopInstanceUid /* out */, + unsigned int& frameNumber /* out */) const + { + if (cursor_.get() != NULL && + frames_.get() != NULL) + { + const size_t cursorIndex = cursor_->GetCurrentIndex(); + const OrthancStone::DicomInstanceParameters& instance = frames_->GetInstanceOfFrame(cursorIndex); + sopInstanceUid = instance.GetSopInstanceUid(); + frameNumber = frames_->GetFrameNumberInInstance(cursorIndex); + return true; + } + else + { + return false; + } + } + + + void SetDeepLearningMask(const std::string& sopInstanceUid, + unsigned int frameNumber, + const Orthanc::ImageAccessor& mask) + { + std::string currentSopInstanceUid; + unsigned int currentFrameNumber; + if (GetCurrentFrame(currentSopInstanceUid, currentFrameNumber) && + sopInstanceUid == currentSopInstanceUid && + frameNumber == currentFrameNumber) + { + deepLearningSopInstanceUid_ = sopInstanceUid; + deepLearningFrameNumber_ = frameNumber; + deepLearningMask_.reset(Orthanc::Image::Clone(mask)); + Redraw(); + } + } + + void SignalSynchronizedBrowsing() { if (synchronizationEnabled_ && @@ -3854,6 +3926,218 @@ } +#include +#include "Worker.pb.h" + +enum DeepLearningState +{ + DeepLearningState_Waiting, + DeepLearningState_Pending, + DeepLearningState_Running +}; + +static DeepLearningState deepLearningState_ = DeepLearningState_Waiting; +static worker_handle deepLearningWorker_; +static std::string deepLearningPendingSopInstanceUid_; +static unsigned int deepLearningPendingFrameNumber_; + +// Forward declaration +static void DeepLearningCallback(char* data, + int size, + void* payload); + +static void SendRequestToWebWorker(const OrthancStone::Messages::Request& request) +{ + std::string s; + if (request.SerializeToString(&s) && + !s.empty()) + { + emscripten_call_worker(deepLearningWorker_, "Execute", &s[0], s.size(), DeepLearningCallback, NULL); + } + else + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Cannot send command to the Web worker"); + } +} + +static void DeepLearningSchedule(const std::string& sopInstanceUid, + unsigned int frameNumber) +{ + if (deepLearningState_ == DeepLearningState_Waiting) + { + LOG(WARNING) << "Starting deep learning on: " << sopInstanceUid << " / " << frameNumber; + + FramesCache::Accessor accessor(*framesCache_, sopInstanceUid, frameNumber); + if (accessor.IsValid() && + accessor.GetImage().GetFormat() == Orthanc::PixelFormat_Float32) + { + const Orthanc::ImageAccessor& image = accessor.GetImage(); + + OrthancStone::Messages::Request request; + request.set_type(OrthancStone::Messages::RequestType::LOAD_IMAGE); + request.mutable_load_image()->set_sop_instance_uid(sopInstanceUid); + request.mutable_load_image()->set_frame_number(frameNumber); + request.mutable_load_image()->set_width(image.GetWidth()); + request.mutable_load_image()->set_height(image.GetHeight()); + + const unsigned int height = image.GetHeight(); + const unsigned int width = image.GetWidth(); + for (unsigned int y = 0; y < height; y++) + { + const float* p = reinterpret_cast(image.GetConstRow(y)); + for (unsigned int x = 0; x < width; x++, p++) + { + request.mutable_load_image()->mutable_values()->Add(*p); + } + } + + deepLearningState_ = DeepLearningState_Running; + SendRequestToWebWorker(request); + } + else + { + LOG(ERROR) << "Cannot access the frame content, maybe a color image?"; + + EM_ASM({ + const customEvent = document.createEvent("CustomEvent"); + customEvent.initCustomEvent("DeepLearningStep", false, false, + { "progress" : "0" }); + window.dispatchEvent(customEvent); + }); + } + } + else + { + deepLearningState_ = DeepLearningState_Pending; + deepLearningPendingSopInstanceUid_ = sopInstanceUid; + deepLearningPendingFrameNumber_ = frameNumber; + } +} + +static void DeepLearningNextStep() +{ + switch (deepLearningState_) + { + case DeepLearningState_Pending: + deepLearningState_ = DeepLearningState_Waiting; + DeepLearningSchedule(deepLearningPendingSopInstanceUid_, deepLearningPendingFrameNumber_); + break; + + case DeepLearningState_Running: + { + OrthancStone::Messages::Request request; + request.set_type(OrthancStone::Messages::RequestType::EXECUTE_STEP); + SendRequestToWebWorker(request); + break; + } + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, "Bad state for deep learning"); + } +} + +static void DeepLearningCallback(char* data, + int size, + void* payload) +{ + try + { + OrthancStone::Messages::Response response; + if (response.ParseFromArray(data, size)) + { + switch (response.type()) + { + case OrthancStone::Messages::ResponseType::INITIALIZED: + DISPATCH_JAVASCRIPT_EVENT("DeepLearningInitialized"); + break; + + case OrthancStone::Messages::ResponseType::PARSED_MODEL: + LOG(WARNING) << "Number of steps in the model: " << response.parse_model().number_of_steps(); + DISPATCH_JAVASCRIPT_EVENT("DeepLearningModelReady"); + break; + + case OrthancStone::Messages::ResponseType::LOADED_IMAGE: + DeepLearningNextStep(); + break; + + case OrthancStone::Messages::ResponseType::STEP_DONE: + { + EM_ASM({ + const customEvent = document.createEvent("CustomEvent"); + customEvent.initCustomEvent("DeepLearningStep", false, false, + { "progress" : $0 }); + window.dispatchEvent(customEvent); + }, + response.step().progress() + ); + + if (response.step().done()) + { + deepLearningState_ = DeepLearningState_Waiting; + + const unsigned int height = response.step().mask().height(); + const unsigned int width = response.step().mask().width(); + + LOG(WARNING) << "SUCCESS! Mask: " << width << "x" << height << " for frame " + << response.step().mask().sop_instance_uid() << " / " + << response.step().mask().frame_number(); + + Orthanc::Image mask(Orthanc::PixelFormat_Grayscale8, width, height, false); + + size_t pos = 0; + for (unsigned int y = 0; y < height; y++) + { + uint8_t* p = reinterpret_cast(mask.GetRow(y)); + for (unsigned int x = 0; x < width; x++, p++, pos++) + { + *p = response.step().mask().values(pos) ? 255 : 0; + } + } + + for (Viewports::iterator it = allViewports_.begin(); it != allViewports_.end(); ++it) + { + assert(it->second != NULL); + it->second->SetDeepLearningMask(response.step().mask().sop_instance_uid(), + response.step().mask().frame_number(), mask); + } + } + else + { + DeepLearningNextStep(); + } + + break; + } + + default: + LOG(ERROR) << "Unsupported response type from the deep learning worker"; + } + } + else + { + LOG(ERROR) << "Bad response received from the deep learning worker"; + } + } + EXTERN_CATCH_EXCEPTIONS; +} + +static void DeepLearningModelLoaded(emscripten_fetch_t *fetch) +{ + try + { + LOG(WARNING) << "Deep learning model loaded: " << fetch->numBytes; + + OrthancStone::Messages::Request request; + request.set_type(OrthancStone::Messages::RequestType::PARSE_MODEL); + request.mutable_parse_model()->mutable_content()->assign(fetch->data, fetch->numBytes); + + emscripten_fetch_close(fetch); // Don't use "fetch" below + SendRequestToWebWorker(request); + } + EXTERN_CATCH_EXCEPTIONS; +} + extern "C" { int main(int argc, char const *argv[]) @@ -3871,11 +4155,55 @@ framesCache_.reset(new FramesCache); osiriXAnnotations_.reset(new OrthancStone::OsiriX::CollectionOfAnnotations); + deepLearningWorker_ = emscripten_create_worker("DeepLearningWorker.js"); + emscripten_call_worker(deepLearningWorker_, "Initialize", NULL, 0, DeepLearningCallback, NULL); + DISPATCH_JAVASCRIPT_EVENT("StoneInitialized"); } EMSCRIPTEN_KEEPALIVE + void LoadDeepLearningModel(const char* uri) + { + try + { + LOG(WARNING) << "Loading deep learning model: " << uri; + + emscripten_fetch_attr_t attr; + emscripten_fetch_attr_init(&attr); + strcpy(attr.requestMethod, "GET"); + attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; + attr.onsuccess = DeepLearningModelLoaded; + attr.onerror = NULL; + emscripten_fetch(&attr, uri); + } + EXTERN_CATCH_EXCEPTIONS; + } + + + EMSCRIPTEN_KEEPALIVE + void ApplyDeepLearningModel(const char* canvas) + { + try + { + boost::shared_ptr viewport = GetViewport(canvas); + + std::string sopInstanceUid; + unsigned int frameNumber; + if (viewport->GetCurrentFrame(sopInstanceUid, frameNumber)) + { + DeepLearningSchedule(sopInstanceUid, frameNumber); + } + else + { + LOG(WARNING) << "No active frame"; + } + } + EXTERN_CATCH_EXCEPTIONS; + } + + + EMSCRIPTEN_KEEPALIVE void SetDicomWebRoot(const char* uri, int useRendered) { diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Resources/WebAssemblySharedLibrary/CMakeLists.txt --- a/OrthancStone/Resources/WebAssemblySharedLibrary/CMakeLists.txt Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Resources/WebAssemblySharedLibrary/CMakeLists.txt Thu Mar 30 17:20:26 2023 +0200 @@ -54,6 +54,7 @@ set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s DISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1") add_definitions( -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 + -DORTHANC_WEBGL2_HEAP_COMPAT=0 ) diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Resources/WebAssemblyUnitTests/CMakeLists.txt --- a/OrthancStone/Resources/WebAssemblyUnitTests/CMakeLists.txt Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Resources/WebAssemblyUnitTests/CMakeLists.txt Thu Mar 30 17:20:26 2023 +0200 @@ -39,6 +39,7 @@ set(WASM_FLAGS "${WASM_FLAGS} -s SAFE_HEAP=1") endif() +set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ENVIRONMENT=web") set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\", \"cwrap\"]'") set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ERROR_ON_UNDEFINED_SYMBOLS=1") set(WASM_LINKER_FLAGS "${WASM_LINKER_FLAGS} -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=268435456") # 256MB + resize @@ -46,6 +47,7 @@ add_definitions( -DDISABLE_DEPRECATED_FIND_EVENT_TARGET_BEHAVIOR=1 -DORTHANC_BUILD_UNIT_TESTS=1 + -DORTHANC_WEBGL2_HEAP_COMPAT=0 ) diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Sources/OpenGL/OpenGLProgram.cpp --- a/OrthancStone/Sources/OpenGL/OpenGLProgram.cpp Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Sources/OpenGL/OpenGLProgram.cpp Thu Mar 30 17:20:26 2023 +0200 @@ -86,6 +86,12 @@ { //ORTHANC_OPENGL_TRACE_CURRENT_CONTEXT("About to call glUseProgram"); glUseProgram(program_); + if (glGetError() != GL_NO_ERROR) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Cannot use successfully compiled OpenGL shader"); + } + ORTHANC_OPENGL_CHECK("glUseProgram"); } diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Sources/OpenGL/OpenGLShader.cpp --- a/OrthancStone/Sources/OpenGL/OpenGLShader.cpp Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Sources/OpenGL/OpenGLShader.cpp Thu Mar 30 17:20:26 2023 +0200 @@ -55,8 +55,10 @@ glCompileShader(shader); ORTHANC_OPENGL_CHECK("glCompileShader"); + GLenum error = glGetError(); + // Check if there were errors - int infoLen = 0; + GLint infoLen = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen); ORTHANC_OPENGL_CHECK("glGetShaderiv"); @@ -65,18 +67,24 @@ std::string infoLog; infoLog.resize(infoLen + 1); glGetShaderInfoLog(shader, infoLen, NULL, &infoLog[0]); - ORTHANC_OPENGL_CHECK("glGetShaderInfoLog"); - ORTHANC_OPENGL_TRACE_CURRENT_CONTEXT("About to call glDeleteShader"); - glDeleteShader(shader); - ORTHANC_OPENGL_CHECK("glDeleteShader"); - throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Error while creating an OpenGL shader: " + infoLog); + if (error) + { + ORTHANC_OPENGL_CHECK("glGetShaderInfoLog"); + ORTHANC_OPENGL_TRACE_CURRENT_CONTEXT("About to call glDeleteShader"); + glDeleteShader(shader); + ORTHANC_OPENGL_CHECK("glDeleteShader"); + + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Error while creating an OpenGL shader: " + infoLog); + } + else + { + LOG(WARNING) << "Warning while creating an OpenGL shader: " << infoLog; + } } - else - { - return shader; - } + + return shader; } } diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Sources/OpenGL/OpenGLTexture.cpp --- a/OrthancStone/Sources/OpenGL/OpenGLTexture.cpp Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Sources/OpenGL/OpenGLTexture.cpp Thu Mar 30 17:20:26 2023 +0200 @@ -24,26 +24,37 @@ #include "OpenGLTexture.h" #include "IOpenGLContext.h" +#include #include #include +#if defined(__EMSCRIPTEN__) +# if !defined(ORTHANC_WEBGL2_HEAP_COMPAT) +# error The macro ORTHANC_WEBGL2_HEAP_COMPAT must be defined +# endif +#endif + namespace OrthancStone { namespace OpenGL { - OpenGLTexture::OpenGLTexture(OpenGL::IOpenGLContext& context) - : width_(0) - , height_(0) - , context_(context) + OpenGLTexture::OpenGLTexture(OpenGL::IOpenGLContext& context) : + texture_(0), + width_(0), + height_(0), + format_(Orthanc::PixelFormat_Grayscale8), + context_(context) { if (!context_.IsContextLost()) { // Generate a texture object glGenTextures(1, &texture_); + ORTHANC_OPENGL_CHECK("glGenTextures()"); + if (texture_ == 0) { throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, - "Cannot create an OpenGL program"); + "Cannot create an OpenGL texture"); } } } @@ -80,56 +91,89 @@ } } - void OpenGLTexture::Load(const Orthanc::ImageAccessor& image, - bool isLinearInterpolation) + void OpenGLTexture::Setup(Orthanc::PixelFormat format, + unsigned int width, + unsigned int height, + bool isLinearInterpolation, + const void* data) { - if (!context_.IsContextLost()) + if (context_.IsContextLost()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "OpenGL context has been lost"); + } + else { glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Disable byte-alignment restriction - if (image.GetPitch() != image.GetBytesPerPixel() * image.GetWidth()) - { - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, - "Unsupported non-zero padding"); - } - // Bind it glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_); - GLenum sourceFormat, internalFormat; - - switch (image.GetFormat()) - { - case Orthanc::PixelFormat_Grayscale8: - sourceFormat = GL_RED; - internalFormat = GL_RED; - break; - - case Orthanc::PixelFormat_RGB24: - sourceFormat = GL_RGB; - internalFormat = GL_RGB; - break; + GLenum sourceFormat, internalFormat, pixelType; + ConvertToOpenGLFormats(sourceFormat, internalFormat, pixelType, format); - case Orthanc::PixelFormat_RGBA32: - sourceFormat = GL_RGBA; - internalFormat = GL_RGBA; - break; - - default: - throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, - "No support for this format in OpenGL textures: " + - std::string(EnumerationToString(image.GetFormat()))); - } - - width_ = image.GetWidth(); - height_ = image.GetHeight(); + format_ = format; + width_ = width; + height_ = height; GLint interpolation = (isLinearInterpolation ? GL_LINEAR : GL_NEAREST); // Load the texture from the image buffer - glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, image.GetWidth(), image.GetHeight(), - 0, sourceFormat, GL_UNSIGNED_BYTE, image.GetConstBuffer()); + +#if defined(__EMSCRIPTEN__) && (ORTHANC_WEBGL2_HEAP_COMPAT == 1) + /** + * This compatibility implementation seems to be necessary + * with WebGL2, at least in Web workers. In such a situation, + * the calls that are referred to as the "new garbage-free + * entry points" in the Emscripten source file + * "upstream/emscripten/src/library_webgl.js" seem to fail, + * because the "Uint8Array" and "Float32Array" seem to be + * incorrectly created. This compatibility reverts to the + * WebGL1 behavior of "library_webgl.js", which requires the + * function "emscriptenWebGLGetTexPixelData" that is defined + * in "upstream/emscripten/src/library_webgl.js" to be + * exported in the linker using option + * "EXTRA_EXPORTED_RUNTIME_METHODS" or + * "EXPORTED_RUNTIME_METHODS". + **/ + EM_ASM({ + var ptr = $0 ? emscriptenWebGLGetTexPixelData($5, $4, $2, $3, $0, $1) : null; + GLctx.texImage2D(GLctx.TEXTURE_2D, 0, $1, $2, $3, 0, $4, $5, ptr); + }, + data, // $0 + internalFormat, // $1 + width, // $2 + height, // $3 + sourceFormat, // $4 + pixelType); // $5 +#else + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, + 0, sourceFormat, pixelType, data); +#endif + + ORTHANC_OPENGL_CHECK("glTexImage2D()"); + +#if !defined(__EMSCRIPTEN__) + /** + * glGetTexLevelParameteriv() was introduced in OpenGL ES 3.1, + * but WebGL 2 only supports OpenGL ES 3.0, so it is not + * available in WebAssembly: + * https://registry.khronos.org/OpenGL-Refpages/es3.1/html/glGetTexLevelParameter.xhtml + **/ + GLint w, h; + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w); + glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h); + if (width != static_cast(w) || + height != static_cast(h)) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "Your GPU cannot create a texture of size " + + boost::lexical_cast(width) + " x " + + boost::lexical_cast(height)); + } +#endif + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, interpolation); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, interpolation); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); @@ -137,12 +181,143 @@ } } + void OpenGLTexture::Load(const Orthanc::ImageAccessor& image, + bool isLinearInterpolation) + { + if (image.GetPitch() != image.GetBytesPerPixel() * image.GetWidth()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "Pitch is not the same as the row size"); + } + else + { + Setup(image.GetFormat(), image.GetWidth(), image.GetHeight(), + isLinearInterpolation, image.GetConstBuffer()); + } + } - void OpenGLTexture::Bind(GLint location) + + void OpenGLTexture::Bind(GLint location) const { glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture_); glUniform1i(location, 0 /* texture unit */); } + + + void OpenGLTexture::BindAsTextureUnit(GLint location, + unsigned int unit) const + { + if (unit >= 32) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + + assert(GL_TEXTURE0 + 1 == GL_TEXTURE1 && + GL_TEXTURE0 + 31 == GL_TEXTURE31); + + glActiveTexture(GL_TEXTURE0 + unit); + glBindTexture(GL_TEXTURE_2D, texture_); + glUniform1i(location, unit /* texture unit */); + } + + + Orthanc::ImageAccessor* OpenGLTexture::Download(Orthanc::PixelFormat format) const + { + if (context_.IsContextLost()) + { + throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError, + "OpenGL context is lost"); + } + + std::unique_ptr target(new Orthanc::Image(format, width_, height_, true)); + assert(target->GetPitch() == width_ * Orthanc::GetBytesPerPixel(format)); + +#if defined(__EMSCRIPTEN__) + /** + * The "glGetTexImage()" is unavailable in WebGL, it is + * necessary to use a framebuffer: + * https://stackoverflow.com/a/15064957 + **/ + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + +#else + glBindTexture(GL_TEXTURE_2D, texture_); + + switch (format) + { + case Orthanc::PixelFormat_Grayscale8: + glGetTexImage(GL_TEXTURE_2D, 0 /* base level */, GL_RED, GL_UNSIGNED_BYTE, target->GetBuffer()); + break; + + case Orthanc::PixelFormat_RGB24: + glGetTexImage(GL_TEXTURE_2D, 0 /* base level */, GL_RGB, GL_UNSIGNED_BYTE, target->GetBuffer()); + break; + + case Orthanc::PixelFormat_RGBA32: + glGetTexImage(GL_TEXTURE_2D, 0 /* base level */, GL_RGBA, GL_UNSIGNED_BYTE, target->GetBuffer()); + break; + + case Orthanc::PixelFormat_Float32: + glGetTexImage(GL_TEXTURE_2D, 0 /* base level */, GL_RED, GL_FLOAT, target->GetBuffer()); + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented); + } +#endif + + return target.release(); + } + + + void OpenGLTexture::SetClampingToZero() + { + glBindTexture(GL_TEXTURE_2D, texture_); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + + GLfloat colorfv[4] = { 0, 0, 0, 0 }; + glTextureParameterfv(texture_, GL_TEXTURE_BORDER_COLOR, colorfv); + } + + + void OpenGLTexture::ConvertToOpenGLFormats(GLenum& sourceFormat, + GLenum& internalFormat, + GLenum& pixelType, + Orthanc::PixelFormat format) + { + switch (format) + { + case Orthanc::PixelFormat_Grayscale8: + sourceFormat = GL_RED; + internalFormat = GL_RED; + pixelType = GL_UNSIGNED_BYTE; + break; + + case Orthanc::PixelFormat_RGB24: + sourceFormat = GL_RGB; + internalFormat = GL_RGB; + pixelType = GL_UNSIGNED_BYTE; + break; + + case Orthanc::PixelFormat_RGBA32: + sourceFormat = GL_RGBA; + internalFormat = GL_RGBA; + pixelType = GL_UNSIGNED_BYTE; + break; + + case Orthanc::PixelFormat_Float32: + sourceFormat = GL_RED; + internalFormat = GL_R32F; // Don't use "GL_RED" here, as it clamps to [0,1] + pixelType = GL_FLOAT; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented, + "No support for this format in OpenGL textures: " + + std::string(EnumerationToString(format))); + } + } } } diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Sources/OpenGL/OpenGLTexture.h --- a/OrthancStone/Sources/OpenGL/OpenGLTexture.h Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Sources/OpenGL/OpenGLTexture.h Thu Mar 30 17:20:26 2023 +0200 @@ -24,6 +24,7 @@ #pragma once #include "OpenGLIncludes.h" +#include "IOpenGLContext.h" #include @@ -34,21 +35,41 @@ { namespace OpenGL { - class IOpenGLContext; - class OpenGLTexture : public boost::noncopyable { private: GLuint texture_; unsigned int width_; unsigned int height_; + + Orthanc::PixelFormat format_; OpenGL::IOpenGLContext& context_; + void Setup(Orthanc::PixelFormat format, + unsigned int width, + unsigned int height, + bool isLinearInterpolation, + const void* data); + public: explicit OpenGLTexture(OpenGL::IOpenGLContext& context); ~OpenGLTexture(); + /** + * Returns the low-level OpenGL handle of the texture. Beware to + * never change the size of the texture using this handle! + **/ + GLuint GetId() const + { + return texture_; + } + + Orthanc::PixelFormat GetFormat() const + { + return format_; + } + unsigned int GetWidth() const { return width_; @@ -59,10 +80,34 @@ return height_; } + void Setup(Orthanc::PixelFormat format, + unsigned int width, + unsigned int height, + bool isLinearInterpolation) + { + Setup(format, width, height, isLinearInterpolation, NULL); + } + void Load(const Orthanc::ImageAccessor& image, bool isLinearInterpolation); - void Bind(GLint location); + void Bind(GLint location) const; + + void BindAsTextureUnit(GLint location, + unsigned int unit) const; + + Orthanc::ImageAccessor* Download(Orthanc::PixelFormat format) const; + + /** + * By default, textures are mirrored at the borders. This + * function will set out-of-image access to zero. + **/ + void SetClampingToZero(); + + static void ConvertToOpenGLFormats(GLenum& sourceFormat, + GLenum& internalFormat, + GLenum& pixelType, + Orthanc::PixelFormat format); }; } } diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp --- a/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.cpp Thu Mar 30 17:20:26 2023 +0200 @@ -45,7 +45,8 @@ bool isContextLost_; public: - explicit PImpl(const std::string& canvasSelector) : + explicit PImpl(const std::string& canvasSelector, + Version version) : canvasSelector_(canvasSelector), isContextLost_(false) { @@ -53,6 +54,20 @@ EmscriptenWebGLContextAttributes attr; emscripten_webgl_init_context_attributes(&attr); + switch (version) + { + case Version_WebGL1: + break; + + case Version_WebGL2: + attr.majorVersion = 2; + attr.minorVersion = 0; + break; + + default: + throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange); + } + // The next line might be necessary to print using // WebGL. Sometimes, if set to "false" (the default value), // the canvas was rendered as a fully white or black @@ -162,7 +177,13 @@ WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvasSelector) : - pimpl_(new PImpl(canvasSelector)) + pimpl_(new PImpl(canvasSelector, Version_WebGL1)) + { + } + + WebAssemblyOpenGLContext::WebAssemblyOpenGLContext(const std::string& canvasSelector, + Version version) : + pimpl_(new PImpl(canvasSelector, version)) { } diff -r d77fea5934fb -r 0d814292a17e OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.h --- a/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.h Thu Mar 30 17:20:01 2023 +0200 +++ b/OrthancStone/Sources/Platforms/WebAssembly/WebAssemblyOpenGLContext.h Thu Mar 30 17:20:26 2023 +0200 @@ -51,6 +51,13 @@ { class WebAssemblyOpenGLContext : public OpenGL::IOpenGLContext { + public: + enum Version + { + Version_WebGL1, + Version_WebGL2 + }; + private: class PImpl; boost::shared_ptr pimpl_; @@ -58,6 +65,9 @@ public: explicit WebAssemblyOpenGLContext(const std::string& canvasSelector); + explicit WebAssemblyOpenGLContext(const std::string& canvasSelector, + Version version); + virtual bool IsContextLost() ORTHANC_OVERRIDE; virtual void MakeCurrent() ORTHANC_OVERRIDE;