diff --git a/.gitignore b/.gitignore index 3f6122b..94b3bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ *.out *.app -deps manual-install-* *.zip -win-spout-installer.versioned.nsi \ No newline at end of file +win-spout-installer.versioned.nsi diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2fa3409 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "deps/Spout2"] + path = deps/Spout2 + url = git@github.com:leadedge/Spout2 diff --git a/README.md b/README.md index 092b810..c8b59a2 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Thanks to the authors of [SPOUT](https://github.com/leadedge/Spout2) for the lib ## Installation -- Go to the [Releases Page](https://github.com/Off-World-Live/obs-spout2-source-plugin/releases) +- Go to the [Releases Page](https://github.com/Off-World-Live/obs-spout2-plugin/releases) - Download the windows installer: `OBS_Spout2_Plugin_Installer.exe` - Run the installer (accepting installation from untrusted source) - Select the `OBS` directory if not the default install location @@ -38,14 +38,21 @@ Thanks to the authors of [SPOUT](https://github.com/leadedge/Spout2) for the lib ## Contributing / Building -- Clone the [main OBS repository](https://github.com/obsproject/obs-studio) +- Clone this repo recursively +``` +git clone --recursive git@github.com:off-world-live/obs-spout2-plugin +``` +- Clone the [main OBS repository](https://github.com/obsproject/obs-studio) recursively. - Carefully follow their [build instructions](https://obsproject.com/wiki/install-instructions#windows-build-directions) ensuring that your `build` folder is `build64` - Add this repo as a submodule inside the plugins folder: `git submodule add git@github.com:Off-World-Live/obs-spout2-source-plugin.git plugins/win-spout` -- Clone Spout [github.com/leadedge/Spout2](https://github.com/leadedge/Spout2) to the folder `deps/Spout2` inside - this directory - Edit the `CMakeLists.txt` file in `/plugins` directory and add `add_subdirectory(win-spout)` inside the `if(WIN32)` block. - Run `Configure`, `Generate` and then `Open Project` in the `CMake Gui` +### Building a release locally + +- Open `git bash` or similar bash terminal interpreter +- Run `./scripts/Release.sh ` +- You should find the executable (installer) and zip file in the main `win-spout` directory ### Building the windows installer - Download the latest version of [NSIS here](https://nsis.sourceforge.io/Download); diff --git a/deps/Spout2 b/deps/Spout2 new file mode 160000 index 0000000..a26f74a --- /dev/null +++ b/deps/Spout2 @@ -0,0 +1 @@ +Subproject commit a26f74a11d43ef74778d73b6741d6805462702c0 diff --git a/source/win-spout-filter.cpp b/source/win-spout-filter.cpp index 946e1ee..ef39d05 100644 --- a/source/win-spout-filter.cpp +++ b/source/win-spout-filter.cpp @@ -20,31 +20,68 @@ struct win_spout_filter { - spoutDX* filter_sender; + // mutex guards accesses to fields in SHARED section + // and any methods on spoutDX* filter_sender. + // Calling obs methods on obs types seems thread-safe. + // trying to avoid calling obs methods while holding our own mutex. + pthread_mutex_t mutex; + + // [SHARED] + spoutDX* filter_sender; // owned by the filter obs_source_t* source_context; - const char* sender_name; - uint32_t width; - uint32_t height; - gs_texrender_t* texrender_curr; - gs_texrender_t* texrender_prev; - gs_texrender_t* texrender_intermediate; - gs_stagesurf_t* stagesurface; - video_t* video_output; - uint8_t* video_data; - uint32_t video_linesize; - obs_video_info video_info; + const char* sender_name; // owned by obs + + // [RENDER] After creation, only accessed on render thread + gs_texrender_t* texrender_curr; // owned by filter + gs_texrender_t* texrender_prev; // " + gs_texrender_t* texrender_intermediate; // " + gs_stagesurf_t* stagesurface; // " + + // set after we successfully init on render thread + bool is_initialised; + // detect that source is still active by setting in _videorender() and clearing in _offscreen_render() + bool is_active; }; -bool openDX11(void* data) +// forward decls +void win_spout_filter_update(void *data, obs_data_t *settings); +void win_spout_filter_destroy(void *data); + +bool init_on_render_thread(struct win_spout_filter *context) { - struct win_spout_filter* context = (win_spout_filter*)data; + if (context->is_initialised) { return true; } + + // Create textures + // Use a Spout-compatible texture format + context->texrender_curr = + gs_texrender_create(GS_BGRA_UNORM, GS_ZS_NONE); + context->texrender_prev = + gs_texrender_create(GS_BGRA_UNORM, GS_ZS_NONE); + context->texrender_intermediate = + gs_texrender_create(GS_BGRA, GS_ZS_NONE); + + // Init Spout context->filter_sender->SetMaxSenders(255); - if (!context->filter_sender->OpenDirectX11()) + + // Get the OBS D3D11 device, rather than creating a new one for each filter. + // If this ends up causing deadlocks or perf issues, can revisit. + ID3D11Device *const d3d_device = (ID3D11Device *)gs_get_device_obj(); + + if (!d3d_device) { + blog(LOG_ERROR, "Failed to retrieve OBS d3d11 device"); + return false; + } + + if (!context->filter_sender->OpenDirectX11(d3d_device)) { blog(LOG_ERROR, "Failed to Open DX11"); return false; } + blog(LOG_INFO, "Opened DX11"); + + context->is_initialised = true; + return true; } @@ -54,8 +91,6 @@ const char* win_spout_filter_getname(void* unused) return obs_module_text("filtername"); } -void win_spout_filter_update(void* data, obs_data_t* settings); - bool win_spout_filter_change_name(obs_properties_t*, obs_property_t*, void* data) { struct win_spout_filter* context = (win_spout_filter*)data; @@ -82,21 +117,38 @@ void win_spout_filter_getdefaults(obs_data_t* defaults) } void win_spout_offscreen_render(void* data, uint32_t cx, uint32_t cy) -{ - +{ UNUSED_PARAMETER(cx); UNUSED_PARAMETER(cy); struct win_spout_filter* context = (win_spout_filter*)data; - obs_source_t* target = obs_filter_get_parent(context->source_context); + // We check if video_render has been called since the last offscreen_render + if (!context->is_active) { return; } + context->is_active = false; + + if (!init_on_render_thread(context)) { + blog(LOG_ERROR, + "Failed to create DX11 context for spout filter!"); + win_spout_filter_destroy(context); + return; + } + + pthread_mutex_lock(&context->mutex); + obs_source_t* source_context = context->source_context; + gs_texrender_t* texrender_intermediate = context->texrender_intermediate; + gs_texrender_t* texrender_curr = context->texrender_curr; + gs_texrender_t* texrender_prev = context->texrender_prev; + pthread_mutex_unlock(&context->mutex); + + obs_source_t* target = obs_filter_get_parent(source_context); if (!target) return; uint32_t width = obs_source_get_base_width(target); uint32_t height = obs_source_get_base_height(target); // Render the target to an intemediate format in sRGB-aware format - gs_texrender_reset(context->texrender_intermediate); - if (gs_texrender_begin(context->texrender_intermediate, width, height)) { + gs_texrender_reset(texrender_intermediate); + if (gs_texrender_begin(texrender_intermediate, width, height)) { struct vec4 background; vec4_zero(&background); @@ -110,12 +162,12 @@ void win_spout_offscreen_render(void* data, uint32_t cx, uint32_t cy) obs_source_video_render(target); gs_blend_state_pop(); - gs_texrender_end(context->texrender_intermediate); + gs_texrender_end(texrender_intermediate); } // Use the default effect to render it back into a format Spout accepts - gs_texrender_reset(context->texrender_curr); - if (gs_texrender_begin(context->texrender_curr, width, height)) + gs_texrender_reset(texrender_curr); + if (gs_texrender_begin(texrender_curr, width, height)) { struct vec4 background; vec4_zero(&background); @@ -128,7 +180,7 @@ void win_spout_offscreen_render(void* data, uint32_t cx, uint32_t cy) // To get sRGB handling, render with the default effect gs_effect_t *effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); - gs_texture_t *tex = gs_texrender_get_texture(context->texrender_intermediate); + gs_texture_t *tex = gs_texrender_get_texture(texrender_intermediate); if (tex) { const bool linear_srgb = gs_get_linear_srgb(); @@ -149,21 +201,34 @@ void win_spout_offscreen_render(void* data, uint32_t cx, uint32_t cy) } gs_blend_state_pop(); - gs_texrender_end(context->texrender_curr); + gs_texrender_end(texrender_curr); + + bool ok = false; gs_texture_t *prev_tex = - gs_texrender_get_texture(context->texrender_prev); + gs_texrender_get_texture(texrender_prev); + ID3D11Texture2D *prev_tex_d3d11 = nullptr; if (prev_tex) { - context->filter_sender->SendTexture(( - ID3D11Texture2D *)gs_texture_get_obj(prev_tex)); + prev_tex_d3d11 = (ID3D11Texture2D *)gs_texture_get_obj(prev_tex); + } + pthread_mutex_lock(&context->mutex); + + if (prev_tex) { + ok = context->filter_sender->SendTexture(prev_tex_d3d11); } // Swap the buffers // Double-buffering avoids the need for a flush, and also fixes // some issues related to G-Sync. - gs_texrender_t *tmp = context->texrender_curr; - context->texrender_curr = context->texrender_prev; - context->texrender_prev = tmp; + + context->texrender_curr = texrender_prev; + context->texrender_prev = texrender_curr; + + pthread_mutex_unlock(&context->mutex); + + if (!ok) { + blog(LOG_ERROR, "Error calling SendTexture()!"); + } } } @@ -173,38 +238,50 @@ void win_spout_filter_update(void* data, obs_data_t* settings) struct win_spout_filter* context = (win_spout_filter*)data; obs_remove_main_render_callback(win_spout_offscreen_render, context); + + const char *sender_name = obs_data_get_string(settings, FILTER_PROP_NAME); + + pthread_mutex_lock(&context->mutex); + context->filter_sender->ReleaseSender(); - context->sender_name = obs_data_get_string(settings, FILTER_PROP_NAME); - context->filter_sender->SetSenderName(context->sender_name); + context->sender_name = sender_name; + context->filter_sender->SetSenderName(sender_name); + + pthread_mutex_unlock(&context->mutex); + obs_add_main_render_callback(win_spout_offscreen_render, context); } void* win_spout_filter_create(obs_data_t* settings, obs_source_t* source) { struct win_spout_filter* context = (win_spout_filter*)bzalloc(sizeof(win_spout_filter)); + // Despite bzalloc I still want to at least initialise pointer fields + context->filter_sender = nullptr; + context->source_context = nullptr; + context->sender_name = nullptr; + context->texrender_curr = nullptr; + context->texrender_prev = nullptr; + context->texrender_intermediate = nullptr; + context->stagesurface = nullptr; + context->is_initialised = false; + context->is_active = false; + + pthread_mutex_init_value(&context->mutex); + if (pthread_mutex_init(&context->mutex, NULL) != 0) { + blog(LOG_ERROR, "Failed to create mutex for spout filter!"); + win_spout_filter_destroy(context); + return nullptr; + } context->source_context = source; - // Use a Spout-compatible texture format - context->texrender_curr = gs_texrender_create(GS_BGRA_UNORM, GS_ZS_NONE); - context->texrender_prev = gs_texrender_create(GS_BGRA_UNORM, GS_ZS_NONE); - context->texrender_intermediate = - gs_texrender_create(GS_BGRA, GS_ZS_NONE); + context->sender_name = obs_data_get_string(settings, FILTER_PROP_NAME); - context->video_data = nullptr; context->filter_sender = new spoutDX; - obs_get_video_info(&context->video_info); win_spout_filter_update(context, settings); - if (openDX11(context)) - { - return context; - } - - blog(LOG_ERROR, "Failed to create spout output!"); - context->filter_sender->CloseDirectX11(); - delete context->filter_sender; + // from this point, need to lock mutex to access context safely return context; } @@ -212,34 +289,61 @@ void win_spout_filter_destroy(void* data) { struct win_spout_filter* context = (win_spout_filter*)data; - context->filter_sender->ReleaseSender(); - context->filter_sender->CloseDirectX11(); - delete context->filter_sender; + if (!context) { + return; + } - if (context) - { - obs_remove_main_render_callback(win_spout_offscreen_render, context); - video_output_close(context->video_output); + obs_remove_main_render_callback(win_spout_offscreen_render, context); + + if (context->filter_sender) { + context->filter_sender->ReleaseSender(); + context->filter_sender->CloseDirectX11(); + delete context->filter_sender; + context->filter_sender = nullptr; + } + + if (context->stagesurface) { gs_stagesurface_unmap(context->stagesurface); gs_stagesurface_destroy(context->stagesurface); + context->stagesurface = nullptr; + } + + if (context->texrender_intermediate) { gs_texrender_destroy(context->texrender_intermediate); + context->texrender_intermediate = nullptr; + } + + if (context->texrender_prev) { gs_texrender_destroy(context->texrender_prev); + context->texrender_prev = nullptr; + } + + if (context->texrender_curr) { gs_texrender_destroy(context->texrender_curr); - bfree(context); + context->texrender_curr = nullptr; } + + pthread_mutex_destroy(&context->mutex); + bfree(context); } void win_spout_filter_tick(void* data, float seconds) { UNUSED_PARAMETER(seconds); struct win_spout_filter* context = (win_spout_filter*)data; - obs_get_video_info(&context->video_info); } void win_spout_filter_videorender(void* data, gs_effect_t* effect) { UNUSED_PARAMETER(effect); struct win_spout_filter* context = (win_spout_filter*)data; + + pthread_mutex_lock(&context->mutex); + + context->is_active = true; + + pthread_mutex_unlock(&context->mutex); + obs_source_skip_video_filter(context->source_context); } diff --git a/source/win-spout-output.cpp b/source/win-spout-output.cpp index 198c071..f29c21d 100644 --- a/source/win-spout-output.cpp +++ b/source/win-spout-output.cpp @@ -19,13 +19,22 @@ struct spout_output obs_output_t* output; const char* senderName; bool output_started; + // mutex guards accesses to rest of context variables, + // and any methods on spoutDX* sender. + // Calling obs methods on obs_output_t* output seems thread-safe. + // trying to avoid calling obs methods while holding our own mutex. pthread_mutex_t mutex; }; +// Forward decls +void win_spout_output_destroy(void *data); + bool init_spout(void* data) { spout_output* context = (spout_output*)data; - + // Enable for debugging spout: + // spoututils::SetSpoutLogLevel(spoututils::SPOUT_LOG_VERBOSE); + // spoututils::EnableSpoutLog(); context->sender->SetMaxSenders(255); if (!context->sender->OpenDirectX11()) { @@ -49,20 +58,6 @@ static void win_spout_output_update(void* data, obs_data_t* settings) context->senderName = obs_data_get_string(settings, "senderName"); } -static void win_spout_output_destroy(void* data) -{ - spout_output* context = (spout_output*)data; - - context->sender->CloseDirectX11(); - delete context->sender; - - if (context) - { - pthread_mutex_destroy(&context->mutex); - bfree(context); - } -} - static void* win_spout_output_create(obs_data_t* settings, obs_output_t* output) { spout_output* context = (spout_output*)bzalloc(sizeof(spout_output)); @@ -71,23 +66,42 @@ static void* win_spout_output_create(obs_data_t* settings, obs_output_t* output) context->output_started = false; context->sender = new spoutDX; - win_spout_output_update(context, settings); + pthread_mutex_init_value(&context->mutex); + if (pthread_mutex_init(&context->mutex, NULL) != 0) { + blog(LOG_ERROR, "Failed to create mutex for spout output!"); + win_spout_output_destroy(context); + return nullptr; + } - if (init_spout(context)) + if (!init_spout(context)) { - pthread_mutex_init_value(&context->mutex); - if (pthread_mutex_init(&context->mutex, NULL) == 0) { - UNUSED_PARAMETER(settings); - return context; - } + blog(LOG_ERROR, "Failed to create spout output!"); + win_spout_output_destroy(context); + return nullptr; } - blog(LOG_ERROR, "Failed to create spout output!"); - context->sender->CloseDirectX11(); - delete context->sender; - win_spout_output_destroy(context); + win_spout_output_update(context, settings); + + // from this point, need to lock mutex to access context safely + return context; +} + +static void win_spout_output_destroy(void *data) +{ + spout_output *context = (spout_output *)data; + + if (!context) { + return; + } + + if (context->sender) { + context->sender->CloseDirectX11(); + delete context->sender; + context->sender = nullptr; + } - return NULL; + pthread_mutex_destroy(&context->mutex); + bfree(context); } bool win_spout_output_start(void* data) @@ -100,19 +114,26 @@ bool win_spout_output_start(void* data) return false; } + pthread_mutex_lock(&context->mutex); + + const char *senderName = context->senderName; context->sender->SetSenderName(context->senderName); - int32_t width = (int32_t)obs_output_get_width(context->output); - int32_t height = (int32_t)obs_output_get_height(context->output); + obs_output_t *output = context->output; - video_t* video = obs_output_video(context->output); + pthread_mutex_unlock(&context->mutex); + + int32_t width = (int32_t)obs_output_get_width(output); + int32_t height = (int32_t)obs_output_get_height(output); + + video_t* video = obs_output_video(output); if (!video) { blog(LOG_ERROR, "Trying to start with no video!"); return false; } - if (!obs_output_can_begin_data_capture(context->output, 0)) + if (!obs_output_can_begin_data_capture(output, 0)) { blog(LOG_ERROR, "Unable to begin data capture!"); return false; @@ -124,19 +145,25 @@ bool win_spout_output_start(void* data) info.width = width; info.height = height; - obs_output_set_video_conversion(context->output, &info); + obs_output_set_video_conversion(output, &info); - context->output_started = obs_output_begin_data_capture(context->output, 0); + bool started = obs_output_begin_data_capture(output, 0); - if (!context->output_started) - { + pthread_mutex_lock(&context->mutex); + + context->output_started = started; + + pthread_mutex_unlock(&context->mutex); + + if (!started) { blog(LOG_ERROR, "Unable to start capture!"); + } else { + blog(LOG_INFO, + "Creating capture with name: %s, width: %i, height: %i", + context->senderName, width, height); } - else - blog(LOG_INFO, "Creating capture with name: %s, width: %i, height: %i", context->senderName, width, height); - - return context->output_started; + return started; } void win_spout_output_stop(void* data, uint64_t ts) @@ -145,12 +172,21 @@ void win_spout_output_stop(void* data, uint64_t ts) spout_output* context = (spout_output*)data; - if (context->output_started) + pthread_mutex_lock(&context->mutex); + bool started = context->output_started; + obs_output_t *output = context->output; + pthread_mutex_unlock(&context->mutex); + + if (started) { - context->output_started = false; + obs_output_end_data_capture(output); + + pthread_mutex_lock(&context->mutex); - obs_output_end_data_capture(context->output); context->sender->ReleaseSender(); + context->output_started = false; + + pthread_mutex_unlock(&context->mutex); } } @@ -158,16 +194,25 @@ void win_spout_output_rawvideo(void* data, struct video_data* frame) { spout_output* context = (spout_output*)data; - if (!context->output_started) + pthread_mutex_lock(&context->mutex); + + bool started = context->output_started; + obs_output_t *output = context->output; + + pthread_mutex_unlock(&context->mutex); + + if (!started) { return; } - int32_t width = (int32_t)obs_output_get_width(context->output); - int32_t height = (int32_t)obs_output_get_height(context->output); + int32_t width = (int32_t)obs_output_get_width(output); + int32_t height = (int32_t)obs_output_get_height(output); pthread_mutex_lock(&context->mutex); + context->sender->SendImage(frame->data[0], width, height); + pthread_mutex_unlock(&context->mutex); } diff --git a/source/win-spout.cpp b/source/win-spout.cpp index 6059a9c..822cde1 100644 --- a/source/win-spout.cpp +++ b/source/win-spout.cpp @@ -33,6 +33,19 @@ struct obs_source_info spout_filter_info; win_spout_output_settings* spout_output_settings; obs_output_t* win_spout_out; +static void spout_obs_event(enum obs_frontend_event event, void *) +{ + if (event == OBS_FRONTEND_EVENT_EXIT) { + if (!win_spout_out) { + return; + } + + obs_output_stop(win_spout_out); + obs_output_release(win_spout_out); + win_spout_out = nullptr; + } +} + bool obs_module_load(void) { // load spout - source @@ -68,6 +81,8 @@ bool obs_module_load(void) auto menu_cb = [] { spout_output_settings->toggle_show_hide(); }; menu_action->connect(menu_action, &QAction::triggered, menu_cb); + obs_frontend_add_event_callback(spout_obs_event, nullptr); + // load spout filter spout_filter_info = create_spout_filter_info(); obs_register_source(&spout_filter_info); @@ -79,8 +94,6 @@ bool obs_module_load(void) void obs_module_unload() { - obs_output_release(win_spout_out); - win_spout_out = nullptr; blog(LOG_INFO, "win-spout unloaded!"); }