gfx-shader: Add file watching and dynamic UI updates

Thanks to the workaround in obs::tools, gfx::shader::shader now supports dynamically rebuilding the properties with new properties without crashing OBS Studio. This effectively allows you to have an up to date view of the current parameters for the shader technique.

Additionally with file watching, live development of shaders is possible at very little cost. Currently only file times and size is looked at every 333ms, but in the future it is possible to also watch for file renames and more.
This commit is contained in:
Michael Fabian 'Xaymar' Dirks 2019-12-22 06:14:26 +01:00
parent bd6b4f2d2a
commit 4c5a7018a3
2 changed files with 128 additions and 143 deletions

View file

@ -19,6 +19,7 @@
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include "obs/obs-tools.hpp"
#include "plugin.hpp" #include "plugin.hpp"
#define ST "Shader" #define ST "Shader"
@ -34,79 +35,132 @@ gfx::shader::shader::shader(obs_source_t* self, shader_mode mode)
: _self(self), _mode(mode), _base_width(1), _base_height(1), _input_a(), _input_b(), : _self(self), _mode(mode), _base_width(1), _base_height(1), _input_a(), _input_b(),
_shader(), _shader_file(), _shader_tech("Draw"), _shader_file_mt(), _shader_file_sz(), _shader_file_tick(0), _shader(), _shader_file(), _shader_tech("Draw"), _shader_file_mt(), _shader_file_sz(), _shader_file_tick(0),
_shader_params_invalid(true),
_width_type(size_type::Percent), _width_value(1.0), _height_type(size_type::Percent), _height_value(1.0), _width_type(size_type::Percent), _width_value(1.0), _height_type(size_type::Percent), _height_value(1.0),
_time(0), _random() _time(0), _random(), _have_current_params(false)
{ {
_random.seed(static_cast<unsigned long long>(time(NULL))); _random.seed(static_cast<unsigned long long>(time(NULL)));
} }
gfx::shader::shader::~shader() {} gfx::shader::shader::~shader() {}
bool gfx::shader::shader::load_shader(std::filesystem::path file) bool gfx::shader::shader::is_shader_different(const std::filesystem::path& file)
{ {
if (!is_shader_different(file)) // Check if the file name differs.
return false; if (file != _shader_file)
return true;
// Is the file write time different?
if (std::filesystem::last_write_time(_shader_file) != _shader_file_mt)
return true;
// Is the file size different?
if (std::filesystem::file_size(_shader_file) != _shader_file_sz)
return true;
return false;
}
bool gfx::shader::shader::is_technique_different(const std::string& tech)
{
// Is the technique different?
if (tech != _shader_tech)
return true;
return false;
}
bool gfx::shader::shader::load_shader(const std::filesystem::path& file, const std::string& tech, bool& shader_dirty,
bool& param_dirty)
{
shader_dirty = is_shader_different(file);
param_dirty = is_technique_different(tech) || shader_dirty;
if (shader_dirty) {
try {
_shader = gs::effect(file); _shader = gs::effect(file);
} catch (...) {
return false;
}
_shader_file = file; _shader_file = file;
_shader_file_mt = std::filesystem::last_write_time(file); _shader_file_mt = std::filesystem::last_write_time(file);
_shader_file_sz = std::filesystem::file_size(file); _shader_file_sz = std::filesystem::file_size(file);
_shader_file_tick = 0; _shader_file_tick = 0;
_shader_params_invalid = true; }
if (param_dirty) {
auto settings =
std::shared_ptr<obs_data_t>(obs_source_get_settings(_self), [](obs_data_t* p) { obs_data_release(p); });
return true; bool have_valid_tech = false;
for (size_t idx = 0; idx < _shader.count_techniques(); idx++) {
if (_shader.get_technique(idx).name() == tech) {
have_valid_tech = true;
break;
}
}
if (have_valid_tech) {
_shader_tech = tech;
} else {
_shader_tech = _shader.get_technique(0).name();
// Update source data.
obs_data_set_string(settings.get(), ST_SHADER_TECHNIQUE, _shader_tech.c_str());
} }
void gfx::shader::shader::load_shader_params() // Clear the shader parameters map and rebuild.
{
if (!_shader_params_invalid)
return;
_shader_params.clear(); _shader_params.clear();
auto etech = _shader.get_technique(_shader_tech);
if (!_shader) for (size_t idx = 0; idx < etech.count_passes(); idx++) {
return; auto pass = etech.get_pass(idx);
if (gs::effect_technique tech = _shader.get_technique(_shader_tech); tech != nullptr) {
for (size_t idx = 0; idx < tech.count_passes(); idx++) {
auto pass = tech.get_pass(idx);
for (size_t vidx = 0; vidx < pass.count_vertex_parameters(); vidx++) { for (size_t vidx = 0; vidx < pass.count_vertex_parameters(); vidx++) {
auto el = pass.get_vertex_parameter(vidx); auto el = pass.get_vertex_parameter(vidx);
if (!el)
continue;
auto fnd = _shader_params.find(el.get_name()); auto fnd = _shader_params.find(el.get_name());
if (fnd != _shader_params.end()) if (fnd != _shader_params.end())
continue; continue;
auto param = gfx::shader::parameter::make_parameter(el, ST_PARAMETERS); auto param = gfx::shader::parameter::make_parameter(el, ST_PARAMETERS);
if (param) if (param) {
_shader_params.insert_or_assign(el.get_name(), param); _shader_params.insert_or_assign(el.get_name(), param);
param->defaults(settings.get());
param->update(settings.get());
}
} }
for (size_t vidx = 0; vidx < pass.count_pixel_parameters(); vidx++) { for (size_t vidx = 0; vidx < pass.count_pixel_parameters(); vidx++) {
auto el = pass.get_pixel_parameter(vidx); auto el = pass.get_pixel_parameter(vidx);
if (!el)
continue;
auto fnd = _shader_params.find(el.get_name()); auto fnd = _shader_params.find(el.get_name());
if (fnd != _shader_params.end()) if (fnd != _shader_params.end())
continue; continue;
auto param = gfx::shader::parameter::make_parameter(el, ST_PARAMETERS); auto param = gfx::shader::parameter::make_parameter(el, ST_PARAMETERS);
if (param) if (param) {
_shader_params.insert_or_assign(el.get_name(), param); _shader_params.insert_or_assign(el.get_name(), param);
param->defaults(settings.get());
param->update(settings.get());
}
} }
} }
} }
_shader_params_invalid = false; return true;
} }
void gfx::shader::shader::properties(obs_properties_t* pr) void gfx::shader::shader::properties(obs_properties_t* pr)
{ {
_have_current_params = false;
{ {
auto grp = obs_properties_create(); auto grp = obs_properties_create();
obs_properties_add_group(pr, ST_SHADER, D_TRANSLATE(ST_SHADER), OBS_GROUP_NORMAL, grp); obs_properties_add_group(pr, ST_SHADER, D_TRANSLATE(ST_SHADER), OBS_GROUP_NORMAL, grp);
@ -118,7 +172,7 @@ void gfx::shader::shader::properties(obs_properties_t* pr)
obs_property_set_modified_callback2( obs_property_set_modified_callback2(
p, p,
[](void* priv, obs_properties_t* props, obs_property_t* prop, obs_data_t* data) noexcept { [](void* priv, obs_properties_t* props, obs_property_t* prop, obs_data_t* data) noexcept {
return reinterpret_cast<gfx::shader::shader*>(priv)->on_shader_changed(props, prop, data); return reinterpret_cast<gfx::shader::shader*>(priv)->on_properties_modified(props, prop, data);
}, },
this); this);
} }
@ -129,7 +183,7 @@ void gfx::shader::shader::properties(obs_properties_t* pr)
obs_property_set_modified_callback2( obs_property_set_modified_callback2(
p, p,
[](void* priv, obs_properties_t* props, obs_property_t* prop, obs_data_t* data) noexcept { [](void* priv, obs_properties_t* props, obs_property_t* prop, obs_data_t* data) noexcept {
return reinterpret_cast<gfx::shader::shader*>(priv)->on_technique_changed(props, prop, data); return reinterpret_cast<gfx::shader::shader*>(priv)->on_properties_modified(props, prop, data);
}, },
this); this);
} }
@ -155,122 +209,49 @@ void gfx::shader::shader::properties(obs_properties_t* pr)
} }
} }
bool gfx::shader::shader::is_shader_different(std::filesystem::path file) bool gfx::shader::shader::on_properties_modified(obs_properties_t* props, obs_property_t* prop, obs_data_t* data)
{ {
// Check if the file name differs. bool shader_dirty = false;
if (file != _shader_file) bool param_dirty = false;
return true;
// Is the file write time different?
if (std::filesystem::last_write_time(_shader_file) != _shader_file_mt) {
return true;
}
// Is the file size different?
if (std::filesystem::file_size(_shader_file) != _shader_file_sz) {
return true;
}
if (!update_shader(data, shader_dirty, param_dirty))
return false; return false;
}
bool gfx::shader::shader::on_shader_changed(obs_properties_t* props, obs_property_t* prop, obs_data_t* data) { // Clear list of techniques and rebuild it.
{ obs_property_t* p_tech_list = obs_properties_get(props, ST_SHADER_TECHNIQUE);
// Load changed shader. obs_property_list_clear(p_tech_list);
update_shader(data); for (size_t idx = 0; idx < _shader.count_techniques(); idx++) {
// Clear list of techniques.
obs_property_t* list = obs_properties_get(props, ST_SHADER_TECHNIQUE);
obs_property_list_clear(list);
// Don't go further if there is no shader.
if (!_shader)
return true;
// Rebuild Technique list.
{
const char* tech_name_c = obs_data_get_string(data, ST_SHADER_TECHNIQUE);
std::string tech_name = tech_name_c ? tech_name_c : "";
bool have_tech = false;
for (size_t idx = 0, idx_end = _shader.count_techniques(); idx < idx_end; idx++) {
auto tech = _shader.get_technique(idx); auto tech = _shader.get_technique(idx);
obs_property_list_add_string(list, tech.name().c_str(), tech.name().c_str()); obs_property_list_add_string(p_tech_list, tech.name().c_str(), tech.name().c_str());
if (tech.name() == tech_name) {
have_tech = true;
} }
} }
if (!have_tech && (_shader.count_techniques() > 0)) { if (param_dirty || !_have_current_params) {
obs_data_set_string(data, ST_SHADER_TECHNIQUE, _shader.get_technique(0).name().c_str());
//on_technique_changed(props, prop, data);
} else if (_shader.count_techniques() == 0) {
obs_data_set_string(data, ST_SHADER_TECHNIQUE, "");
}
}
return true;
}
bool gfx::shader::shader::on_technique_changed(obs_properties_t* props, obs_property_t* prop, obs_data_t* data)
{
// Clear parameter options. // Clear parameter options.
auto grp = obs_property_group_content(obs_properties_get(props, ST_PARAMETERS)); auto grp = obs_property_group_content(obs_properties_get(props, ST_PARAMETERS));
while (true) { for (auto p = obs_properties_first(grp); p != nullptr; p = obs_properties_first(grp)) {
if (auto p = obs_properties_first(grp); p != nullptr) { obs::tools::obs_properties_remove_by_name(grp, obs_property_name(p));
std::string name = obs_property_name(p) ? obs_property_name(p) : "";
obs_properties_remove_by_name(grp, name.c_str());
} else {
break;
} }
}
// Don't go further if there is no shader.
if (!_shader)
return true;
// Load technique.
update_technique(data);
// Rebuild new parameters. // Rebuild new parameters.
for (auto kv : _shader_params) { for (auto kv : _shader_params) {
kv.second->properties(grp, data); kv.second->properties(grp, data);
kv.second->defaults(data);
kv.second->update(data); kv.second->update(data);
} }
return true;
} }
void gfx::shader::shader::update_shader(obs_data_t* data) _have_current_params = true;
{ return shader_dirty || param_dirty || !_have_current_params;
}
bool gfx::shader::shader::update_shader(obs_data_t* data, bool& shader_dirty, bool& param_dirty)
{ {
const char* file_c = obs_data_get_string(data, ST_SHADER_FILE); const char* file_c = obs_data_get_string(data, ST_SHADER_FILE);
std::string file = file_c ? file_c : ""; std::string file = file_c ? file_c : "";
if (file != "") { const char* tech_c = obs_data_get_string(data, ST_SHADER_TECHNIQUE);
try { std::string tech = tech_c ? tech_c : "Draw";
if (!load_shader(file))
return;
} catch (const std::exception& ex) {
P_LOG_ERROR("Failed to load shader: %s.", ex.what());
_shader.reset();
} catch (...) {
P_LOG_ERROR("Failed to load shader.");
_shader.reset();
}
} else {
_shader.reset();
}
}
}
void gfx::shader::shader::update_technique(obs_data_t* data) return load_shader(file, tech, shader_dirty, param_dirty);
{
const char* shader_tech_c = obs_data_get_string(data, ST_SHADER_TECHNIQUE);
std::string shader_tech = shader_tech_c ? shader_tech_c : "";
if ((_shader_params_invalid) || (_shader_tech != shader_tech)) {
_shader_tech = shader_tech;
_shader_params_invalid = true;
load_shader_params();
}
} }
inline std::pair<gfx::shader::size_type, double_t> parse_text_as_size(const char* text) inline std::pair<gfx::shader::size_type, double_t> parse_text_as_size(const char* text)
@ -290,8 +271,8 @@ inline std::pair<gfx::shader::size_type, double_t> parse_text_as_size(const char
void gfx::shader::shader::update(obs_data_t* data) void gfx::shader::shader::update(obs_data_t* data)
{ {
update_shader(data); bool v1, v2;
update_technique(data); update_shader(data, v1, v2);
{ {
auto sz_x = parse_text_as_size(obs_data_get_string(data, ST_SHADER_SIZE_WIDTH)); auto sz_x = parse_text_as_size(obs_data_get_string(data, ST_SHADER_SIZE_WIDTH));
@ -366,6 +347,13 @@ uint32_t gfx::shader::shader::height()
bool gfx::shader::shader::tick(float_t time) bool gfx::shader::shader::tick(float_t time)
{ {
_shader_file_tick = static_cast<float_t>(static_cast<double_t>(_shader_file_tick) + static_cast<double_t>(time));
if (_shader_file_tick >= 1.0f / 3.0f) {
_shader_file_tick -= 1.0f / 3.0f;
bool v1, v2;
load_shader(_shader_file, _shader_tech, v1, v2);
}
// Update State // Update State
_time += time; _time += time;

View file

@ -67,7 +67,6 @@ namespace gfx {
std::filesystem::file_time_type _shader_file_mt; std::filesystem::file_time_type _shader_file_mt;
uintmax_t _shader_file_sz; uintmax_t _shader_file_sz;
float_t _shader_file_tick; float_t _shader_file_tick;
bool _shader_params_invalid;
std::map<std::string, std::shared_ptr<parameter>> _shader_params; std::map<std::string, std::shared_ptr<parameter>> _shader_params;
// Options // Options
@ -79,26 +78,24 @@ namespace gfx {
// Cache // Cache
float_t _time; float_t _time;
std::mt19937_64 _random; std::mt19937_64 _random;
bool _have_current_params;
public: public:
shader(obs_source_t* self, shader_mode mode); shader(obs_source_t* self, shader_mode mode);
~shader(); ~shader();
bool load_shader(std::filesystem::path file); bool is_shader_different(const std::filesystem::path& file);
void load_shader_params(); bool is_technique_different(const std::string& tech);
bool load_shader(const std::filesystem::path& file, const std::string& tech, bool& shader_dirty,
bool& param_dirty);
void properties(obs_properties_t* props); void properties(obs_properties_t* props);
bool is_shader_different(std::filesystem::path file); bool on_properties_modified(obs_properties_t* props, obs_property_t* prop, obs_data_t* data);
bool on_shader_changed(obs_properties_t* props, obs_property_t* prop, obs_data_t* data); bool update_shader(obs_data_t* data, bool& shader_dirty, bool& param_dirty);
bool on_technique_changed(obs_properties_t* props, obs_property_t* prop, obs_data_t* data);
void update_shader(obs_data_t* data);
void update_technique(obs_data_t* data);
void update(obs_data_t* data); void update(obs_data_t* data);