Skip to content

Commit

Permalink
Merge pull request #52 from millenium-cyborg/fix/thread-safety
Browse files Browse the repository at this point in the history
Thread safety fixes to address potential crashes and freezes
  • Loading branch information
campbellwmorgan authored Feb 8, 2023
2 parents ffe539f + b251392 commit 3eb5e3d
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 112 deletions.
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
*.out
*.app

deps
manual-install-*
*.zip
win-spout-installer.versioned.nsi
win-spout-installer.versioned.nsi
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "deps/Spout2"]
path = deps/Spout2
url = [email protected]:leadedge/Spout2
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 [email protected]: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 [email protected]: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 <version number>`
- 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);
Expand Down
1 change: 1 addition & 0 deletions deps/Spout2
Submodule Spout2 added at a26f74
222 changes: 163 additions & 59 deletions source/win-spout-filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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);
Expand All @@ -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();

Expand All @@ -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()!");
}
}
}

Expand All @@ -173,73 +238,112 @@ 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;
}

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

Expand Down
Loading

0 comments on commit 3eb5e3d

Please sign in to comment.