Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5282443
wrappers: always hand the effect zero-filled channels (never null)
jcelerier Jul 2, 2026
9c97180
touchdesigner: fix crash on audio->control analyzers (zero audio-outp…
jcelerier Jul 3, 2026
34a26d8
tooling/td sweep: record the crash stage (created/precook/cooked)
jcelerier Jul 3, 2026
45418f6
golden: one-at-a-time case sweep (enum modes + range endpoints) per o…
jcelerier Jul 3, 2026
6c644ce
python: golden differential harness + audio-path & binding fixes (pha…
jcelerier Jul 3, 2026
eb3be97
packaging: single-object Max/Pd packages now carry the generated help…
jcelerier Jul 3, 2026
b830e37
python: fix per-sample audio heap corruption + uninitialised controls…
jcelerier Jul 3, 2026
1342b31
python: wire host callbacks in process() path + expose obj.inputs/out…
jcelerier Jul 3, 2026
93ec667
gstreamer: golden differential harness (headless gst-launch)
jcelerier Jul 3, 2026
9819418
tooling/max: golden differential harness for the Max/MSP backend
jcelerier Jul 3, 2026
34ae6cf
tooling/pd: golden differential harness (headless single-block audio …
jcelerier Jul 3, 2026
f002024
touchdesigner: capture + diff TOP texture output (surfaces a binding …
jcelerier Jul 3, 2026
4667241
gstreamer: capture + diff video texture output (surfaces a caps gap)
jcelerier Jul 3, 2026
f7dbd50
tooling/max: render audio via the Non-Real-Time driver (real sample d…
jcelerier Jul 3, 2026
077d2ec
gstreamer: fix texture filters collapsing src caps to 1x1
jcelerier Jul 3, 2026
8c3b0c8
wrappers: set dynamic frame-port channel count from the buffer (fixes…
jcelerier Jul 3, 2026
4c0774b
touchdesigner: texture output is not verifiable headless (not a bindi…
jcelerier Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmake/avendish.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ function(avnd_make_texture)
avnd_make_golden(${ARGV})
_avnd_dispatch_backend(fuzz ${ARGV})
avnd_make_ossia(${ARGV})
_avnd_dispatch_backend(python ${ARGV})
_avnd_dispatch_backend(max ${ARGV})
_avnd_dispatch_backend(gstreamer ${ARGV} PROCESSOR_TYPE TEXTURE)
_avnd_dispatch_backend(touchdesigner ${ARGV} PROCESSOR_TYPE TOP)
Expand All @@ -360,6 +361,7 @@ function(avnd_make_buffer)

avnd_make_golden(${ARGV})
_avnd_dispatch_backend(fuzz ${ARGV})
_avnd_dispatch_backend(python ${ARGV})
_avnd_dispatch_backend(godot ${ARGV} PROCESSOR_TYPE BUFFER)
endfunction()

Expand Down
16 changes: 16 additions & 0 deletions cmake/avendish.max.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,22 @@ function(avnd_create_max_package)
)
endif()

# Copy the interactive help patch into the package's help/ folder: Max
# searches <package>/help/ for <name>.maxhelp. Best-effort -- the help patch
# may be absent on some toolchains and must not fail packaging.
get_target_property(_maxhelp ${_external} AVND_MAX_HELP)
if(_maxhelp)
if(TARGET "${_external}_help")
add_dependencies("${_external}" "${_external}_help")
endif()
get_property(_pkgdir GLOBAL PROPERTY AVND_PACKAGING_DIR)
add_custom_command(TARGET ${_external} POST_BUILD
COMMAND ${CMAKE_COMMAND}
"-DSRC=${CMAKE_BINARY_DIR}/${_maxhelp}" "-DDST=${_pkg}/help"
-P "${_pkgdir}/avendish.packaging.copy_optional.cmake"
VERBATIM)
endif()


# Copy the external (fairly platform-specific)
if(WIN32)
Expand Down
17 changes: 17 additions & 0 deletions cmake/avendish.pd.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,23 @@ function(avnd_create_pd_package)
add_custom_command(TARGET ${_external} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "${_external_bin}" "${_pkg}/"
)

# Ship the interactive help patch as a sibling of the external: Pd opens
# <c_name>-help.pd when you right-click the object, and searches the same
# folder the external lives in. Best-effort -- the help patch may be absent
# on some toolchains and must not fail packaging.
get_target_property(_pd_help ${_external} AVND_PD_HELP)
if(_pd_help)
if(TARGET "${_external}_help")
add_dependencies("${_external}" "${_external}_help")
endif()
get_property(_pkgdir GLOBAL PROPERTY AVND_PACKAGING_DIR)
add_custom_command(TARGET ${_external} POST_BUILD
COMMAND ${CMAKE_COMMAND}
"-DSRC=${CMAKE_BINARY_DIR}/${_pd_help}" "-DDST=${_pkg}"
-P "${_pkgdir}/avendish.packaging.copy_optional.cmake"
VERBATIM)
endif()
endforeach()

# Copy the support files
Expand Down
164 changes: 129 additions & 35 deletions include/avnd/binding/golden/run.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,39 @@ std::string clean_name(int index)
return std::string{nm};
}

// Set a deterministic value on an input control field, then append
// {index, name, value} to the ordered `arr` (order matches the dump).
// One test case = "override control #index with `value`" (index < 0 => the base
// case: every control at its default midpoint / first-enum / true). We sweep one
// control at a time (OAT): the base case, then each enum mode and each numeric
// range endpoint, holding the others at their default. `value` carries the enum
// ordinal or the range endpoint as a double; string/bool overrides are unused.
struct case_override
{
int index = -1;
double value = 0.0;
};

// Set a deterministic value on an input control field (honouring `ov` when it
// targets this control), then append {index, name, value} to the ordered `arr`.
template <typename F>
void set_and_record_control(F& field, dump_json::document& jdoc, dump_json::value arr, int index)
void set_and_record_control(
F& field, dump_json::document& jdoc, dump_json::value arr, int index,
const case_override& ov)
{
using vt = std::decay_t<decltype(field.value)>;
const bool over = (ov.index == index);
auto node = jdoc.make_node();
node["index"] = index;
node["name"] = clean_name<F>(index);
if constexpr(std::is_same_v<vt, bool>)
{
field.value = true;
node["value"] = true;
field.value = over ? (ov.value != 0.0) : true;
node["value"] = (bool)field.value;
}
else if constexpr(std::floating_point<vt>)
{
if constexpr(avnd::parameter_with_minmax_range<F>)
if(over)
field.value = static_cast<vt>(ov.value);
else if constexpr(avnd::parameter_with_minmax_range<F>)
{
constexpr auto r = avnd::get_range<F>();
field.value = static_cast<vt>(r.min + 0.5 * (r.max - r.min));
Expand All @@ -95,12 +111,14 @@ void set_and_record_control(F& field, dump_json::document& jdoc, dump_json::valu
}
else if constexpr(std::is_enum_v<vt>)
{
field.value = static_cast<vt>(0);
field.value = over ? static_cast<vt>((int)ov.value) : static_cast<vt>(0);
node["value"] = static_cast<int>(field.value);
}
else if constexpr(std::integral<vt>)
{
if constexpr(avnd::parameter_with_minmax_range<F>)
if(over)
field.value = static_cast<vt>((std::int64_t)ov.value);
else if constexpr(avnd::parameter_with_minmax_range<F>)
{
constexpr auto r = avnd::get_range<F>();
field.value = static_cast<vt>(r.min + (r.max - r.min) / 2);
Expand All @@ -121,6 +139,46 @@ void set_and_record_control(F& field, dump_json::document& jdoc, dump_json::valu
arr.push_back(node);
}

// Enumerate the OAT test cases for T: the base case, then for every enum input a
// case per non-zero mode, and for every ranged numeric input a case at min and
// at max. Bounded (sum, not product), so it stays small.
template <typename T>
std::vector<case_override> enumerate_cases()
{
std::vector<case_override> cases;
cases.push_back({-1, 0.0}); // base
if constexpr(avnd::parameter_input_introspection<T>::size > 0)
{
avnd::effect_container<T> probe;
avnd::init_controls(probe);
int idx = 0;
avnd::parameter_input_introspection<T>::for_all(
avnd::get_inputs(probe), [&](auto& field) {
using F = std::decay_t<decltype(field)>;
using vt = std::decay_t<decltype(field.value)>;
if constexpr(std::is_enum_v<vt>)
{
constexpr int C = (int)avnd::get_enum_choices_count<F>();
for(int c = 1; c < C; ++c)
cases.push_back({idx, (double)c});
}
else if constexpr(
(std::floating_point<vt> || std::integral<vt>)&&avnd::
parameter_with_minmax_range<F>)
{
constexpr auto r = avnd::get_range<F>();
if((double)r.min != (double)r.max)
{
cases.push_back({idx, (double)r.min});
cases.push_back({idx, (double)r.max});
}
}
idx++;
});
}
return cases;
}

// Append an output control field's value as {index, name, value}.
template <typename F>
void record_output_control(F& field, dump_json::document& jdoc, dump_json::value arr, int index)
Expand All @@ -140,26 +198,11 @@ void record_output_control(F& field, dump_json::document& jdoc, dump_json::value
arr.push_back(node);
}

// Run ONE test case (fresh effect, override `ov` applied) and write its inputs
// and outputs into `case_node`. Throws on failure (caught by run()).
template <typename T>
int run(std::string_view path)
void run_case(dump_json::document& jdoc, dump_json::value case_node, const case_override& ov)
{
dump_json::document jdoc;
auto root = jdoc.root();
root["c_name"] = avnd::get_c_name<T>();
auto meta = root["meta"];
meta["seed"] = k_seed;
meta["frames"] = k_frames;
meta["rate"] = k_rate;

auto write = [&](std::string_view status) {
root["meta"]["status"] = status;
std::ofstream f{std::string{path}};
f << jdoc.dump();
return 0;
};

try
{
avnd::effect_container<T> effect;
avnd::process_adapter<T> processor;
avnd::audio_channel_manager<T> channels{effect};
Expand Down Expand Up @@ -227,15 +270,15 @@ int run(std::string_view path)
control_buffers.reserve_space(effect, k_frames);

// --- inputs ---
auto in = root["inputs"];
auto in = case_node["inputs"];
auto in_controls = in["controls"];
in_controls.ensure_array();
if constexpr(avnd::parameter_input_introspection<T>::size > 0)
{
int idx = 0;
avnd::parameter_input_introspection<T>::for_all(
avnd::get_inputs(effect), [&](auto& field) {
set_and_record_control(field, jdoc, in_controls, idx++);
set_and_record_control(field, jdoc, in_controls, idx++, ov);
});
}

Expand Down Expand Up @@ -282,7 +325,7 @@ int run(std::string_view path)
avnd::span<float*>{out_ptrs.data(), (std::size_t)out_N}, k_frames);

// --- outputs ---
auto out = root["outputs"];
auto out = case_node["outputs"];
auto out_controls = out["controls"];
out_controls.ensure_array();
if constexpr(avnd::parameter_output_introspection<T>::size > 0)
Expand Down Expand Up @@ -333,17 +376,68 @@ int run(std::string_view path)
out_tex.push_back(node);
});
}
}

return write("ok");
}
catch(const std::exception& e)
template <typename T>
int run(std::string_view path)
{
dump_json::document jdoc;
auto root = jdoc.root();
root["c_name"] = avnd::get_c_name<T>();
auto meta = root["meta"];
meta["seed"] = k_seed;
meta["frames"] = k_frames;
meta["rate"] = k_rate;

auto finish = [&](std::string_view status) {
root["meta"]["status"] = status;
std::ofstream f{std::string{path}};
f << jdoc.dump();
return 0;
};

auto cases_arr = root["cases"];
cases_arr.ensure_array();

std::vector<case_override> overrides;
try
{
root["meta"]["error"] = std::string_view{e.what()};
return write("error");
overrides = enumerate_cases<T>();
}
catch(...)
{
return write("error");
// If we cannot even enumerate, fall back to the single base case.
overrides = {{-1, 0.0}};
}
meta["num_cases"] = (int)overrides.size();

bool all_ok = true;
int ci = 0;
for(const auto& ov : overrides)
{
auto cnode = jdoc.make_node();
cnode["index"] = ci++;
cnode["override_control"] = ov.index;
cnode["override_value"] = ov.value;
try
{
run_case<T>(jdoc, cnode, ov);
cnode["status"] = "ok";
}
catch(const std::exception& e)
{
cnode["status"] = "error";
cnode["error"] = std::string_view{e.what()};
all_ok = false;
}
catch(...)
{
cnode["status"] = "error";
all_ok = false;
}
cases_arr.push_back(cnode);
}

return finish(all_ok ? "ok" : "partial");
}
}
36 changes: 33 additions & 3 deletions include/avnd/binding/gstreamer/texture_filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,39 @@ struct element<T> : property_handler
{
GST_DEBUG_OBJECT(this, "fixate_caps direction=%d", direction);

// For dynamic resolution effects, we cannot predetermine output size
// Start with input size as baseline, actual size will be handled in transform
GstCaps* result = gst_caps_fixate(gst_caps_copy(othercaps));
// Size-preserving default: the src pad template advertises width/height as
// [1,MAX], so a plain gst_caps_fixate() would collapse the output to 1x1
// (the minimum of the range). Copy the fixed dimensions from the input
// (`caps`) onto the output (`othercaps`) so that a filter defaults to the
// input resolution. Effects that actually change size (e.g. a downscaler)
// still renegotiate the concrete output caps at runtime in transform().
othercaps = gst_caps_make_writable(othercaps);

if(gst_caps_get_size(caps) > 0 && gst_caps_get_size(othercaps) > 0)
{
GstStructure* ins = gst_caps_get_structure(caps, 0);
gint width = 0, height = 0, fps_n = 0, fps_d = 1;
const gboolean have_w = gst_structure_get_int(ins, "width", &width);
const gboolean have_h = gst_structure_get_int(ins, "height", &height);
const gboolean have_fps
= gst_structure_get_fraction(ins, "framerate", &fps_n, &fps_d);

const guint n = gst_caps_get_size(othercaps);
for(guint i = 0; i < n; i++)
{
GstStructure* outs = gst_caps_get_structure(othercaps, i);
if(have_w)
gst_structure_fixate_field_nearest_int(outs, "width", width);
if(have_h)
gst_structure_fixate_field_nearest_int(outs, "height", height);
if(have_fps)
gst_structure_fixate_field_nearest_fraction(
outs, "framerate", fps_n, fps_d);
}
}

// Fixate any remaining fields (format etc.)
GstCaps* result = gst_caps_fixate(othercaps);
return result;
}

Expand Down
Loading
Loading