diff --git a/src/ossia-qt/serial/serial_protocol.hpp b/src/ossia-qt/serial/serial_protocol.hpp index 07a848c48bb..4d6e8b989de 100644 --- a/src/ossia-qt/serial/serial_protocol.hpp +++ b/src/ossia-qt/serial/serial_protocol.hpp @@ -22,7 +22,6 @@ #include #include -#include #include #include diff --git a/src/ossia/audio/pipewire_protocol.hpp b/src/ossia/audio/pipewire_protocol.hpp index 6d1e47e761f..f24e48543bf 100644 --- a/src/ossia/audio/pipewire_protocol.hpp +++ b/src/ossia/audio/pipewire_protocol.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -58,6 +59,7 @@ class libpipewire decltype(&::pw_filter_add_port) filter_add_port{}; decltype(&::pw_filter_destroy) filter_destroy{}; decltype(&::pw_filter_connect) filter_connect{}; + decltype(&::pw_filter_disconnect) filter_disconnect{}; decltype(&::pw_filter_get_dsp_buffer) filter_get_dsp_buffer{}; static const libpipewire& instance() @@ -114,6 +116,8 @@ class libpipewire = library.symbol("pw_filter_add_port"); filter_destroy = library.symbol("pw_filter_destroy"); filter_connect = library.symbol("pw_filter_connect"); + filter_disconnect + = library.symbol("pw_filter_disconnect"); filter_get_dsp_buffer = library.symbol( "pw_filter_get_dsp_buffer"); @@ -144,6 +148,7 @@ class libpipewire assert(filter_add_port); assert(filter_destroy); assert(filter_connect); + assert(filter_disconnect); assert(filter_get_dsp_buffer); } }; @@ -323,20 +328,40 @@ struct pipewire_context pw_registry_add_listener( this->registry, &this->registry_listener, ®istry_events, this); - synchronize(); + if(!synchronize()) + { + ossia::logger().error( + "PipeWire: initial synchronize() failed — context marked as broken"); + // m_broken is already set by synchronize(); leave members as-is so the + // destructor still cleans them up, but the factory will see broken() + // and rebuild the context rather than use this one. + return; + } } int pending{}; int done{}; - void synchronize() + int error_state{}; + bool m_broken{}; + + // Once broken, the shared core connection is considered unrecoverable: + // the factory will tear this context down and build a new one rather + // than attempting to reuse it. + bool broken() const noexcept { return m_broken; } + + // Round-trip to the PipeWire daemon with a hard deadline. Returns true + // on success; on timeout or protocol error, logs, marks the context as + // broken, and returns false. Callers must check the return value and + // bail out of their own wait loops — we must never spin forever against + // a wedged daemon. + bool synchronize() { pending = 0; done = 0; + error_state = 0; - if(!core) - return; - - spa_hook core_listener; + if(!core || m_broken) + return false; static constexpr struct pw_core_events core_events = { .version = PW_VERSION_CORE_EVENTS, @@ -347,11 +372,17 @@ struct pipewire_context if(id == PW_ID_CORE && seq == self.pending) { self.done = 1; - libpipewire::instance().main_loop_quit(self.main_loop); } }, .ping = {}, - .error = {}, + .error = + [](void* object, uint32_t id, int seq, int res, const char* message) { + auto& self = *(pipewire_context*)object; + ossia::logger().error( + "PipeWire: core error id={} seq={} res={} ({}): {}", id, seq, res, + spa_strerror(res), message ? message : ""); + self.error_state = 1; + }, .remove_id = {}, .bound_id = {}, .add_mem = {}, @@ -361,19 +392,63 @@ struct pipewire_context #endif }; + spa_hook core_listener; spa_zero(core_listener); pw_core_add_listener(core, &core_listener, &core_events, this); + // RAII: guarantee the listener is removed on every exit path so the + // core never calls into a dead stack frame (the hook lives on the stack). + struct hook_guard + { + spa_hook* h; + ~hook_guard() { spa_hook_remove(h); } + } guard{&core_listener}; + pending = pw_core_sync(core, PW_ID_CORE, 0); - while(!done) + + using clk = std::chrono::steady_clock; + constexpr auto timeout = std::chrono::seconds(2); + const auto deadline = clk::now() + timeout; + + while(!done && !error_state) { - pw.main_loop_run(this->main_loop); + const auto now = clk::now(); + if(now >= deadline) + { + ossia::logger().error( + "PipeWire: synchronize() timed out after {} s waiting for core sync", + int(std::chrono::duration_cast(timeout).count())); + error_state = 1; + break; + } + + const auto remaining + = std::chrono::duration_cast(deadline - now).count(); + const int iter_ms = int(remaining < 50 ? remaining : 50); + + const int r = pw_loop_iterate(lp, iter_ms); + if(r < 0) + { + ossia::logger().error( + "PipeWire: pw_loop_iterate failed: {}", spa_strerror(r)); + error_state = 1; + break; + } + } + + if(error_state || !done) + { + m_broken = true; + return false; } - spa_hook_remove(&core_listener); + return true; } pw_proxy* link_ports(uint32_t out_port, uint32_t in_port) { + if(m_broken) + return nullptr; + auto props = pw.properties_new( PW_KEY_LINK_OUTPUT_PORT, std::to_string(out_port).c_str(), PW_KEY_LINK_INPUT_PORT, std::to_string(in_port).c_str(), nullptr); @@ -389,7 +464,14 @@ struct pipewire_context return nullptr; } - synchronize(); + if(!synchronize()) + { + // sync failed: the daemon may or may not have committed the create. + // Destroy our local proxy (best-effort, won't hang) and bail out. + pw.proxy_destroy(proxy); + pw.properties_free(props); + return nullptr; + } pw.properties_free(props); return proxy; } @@ -623,14 +705,27 @@ class pipewire_audio_protocol : public audio_engine throw std::runtime_error("PipeWire: cannot connect"); } - // Wait until everything is registered with PipeWire - this->loop->synchronize(); + // Wait until everything is registered with PipeWire. Every synchronize() + // call is bounded by a deadline inside the context; if any of them fail + // we abort the construction without setting activated, leaving an inert + // engine that running() will report as false. + if(!this->loop->synchronize()) + { + ossia::logger().error( + "PipeWire: synchronize() failed after filter_connect — engine inactive"); + return; + } { int k = 0; auto node_id = filter_node_id(); while(node_id == 4294967295) { - this->loop->synchronize(); + if(!this->loop->synchronize()) + { + ossia::logger().error( + "PipeWire: synchronize() failed while waiting for node id"); + return; + } node_id = filter_node_id(); if(k++; k > 100) @@ -645,7 +740,12 @@ class pipewire_audio_protocol : public audio_engine while(this_node.inputs.size() < num_local_ins || this_node.outputs.size() < num_local_outs) { - this->loop->synchronize(); + if(!this->loop->synchronize()) + { + ossia::logger().error( + "PipeWire: synchronize() failed while waiting for ports"); + return; + } if(k++; k > 100) return; } @@ -779,10 +879,50 @@ class pipewire_audio_protocol : public audio_engine auto& pw = libpipewire::instance(); + // Tear-down ordering matters here. pw_filter_connect with + // PW_FILTER_FLAG_RT_PROCESS schedules on_process on PipeWire's data + // (RT) loop, which is a different thread from the main/control loop + // we're running on. If we destroy link proxies or the filter while + // the data loop is still dispatching on_process against them, we race + // the RT thread and corrupt PipeWire's internal state — which can + // wedge the shared core connection and leave the app unrecoverable + // short of a full restart. + // + // Correct sequence: + // 1. pw_filter_disconnect: synchronously deactivates the node and + // tears down its data-loop scheduling, so on_process will no + // longer fire for this filter. + // 2. synchronize: round-trip with the daemon so the disconnect has + // fully propagated before we touch any proxies the RT path may + // have referenced. + // 3. destroy link proxies — now safe. + // 4. pw_filter_destroy — frees the local filter impl. It detects + // the prior disconnect and skips re-doing it. + // 5. synchronize — make sure the destroys have landed before we + // return and potentially let the shared context keep running. + if(this->filter) + { + if(int res = pw.filter_disconnect(this->filter); res < 0) + { + ossia::logger().warn( + "PipeWire: filter_disconnect failed: {}", spa_strerror(res)); + } + + // Best-effort sync; if this fails the context will be marked broken + // and the factory will rebuild it on the next make_engine call. We + // still continue the tear-down to free our local resources. + loop->synchronize(); + } + for(auto link : this->links) pw.proxy_destroy(link); + this->links.clear(); - pw.filter_destroy(this->filter); + if(this->filter) + { + pw.filter_destroy(this->filter); + this->filter = nullptr; + } loop->synchronize(); activated = false; @@ -859,19 +999,23 @@ class pipewire_audio_protocol : public audio_engine return; auto& self = *(pipewire_audio_protocol*)userdata; - uint32_t nframes = position->clock.duration; - double current_time_ns = position->clock.nsec * 1e-9; - while(nframes >= self.effective_buffer_size) + const uint32_t nframes = position->clock.duration; + const double current_time_ns = position->clock.nsec * 1e-9; + + // ossia's audio graph runs at a fixed block size; since we force the quantum + // via PW_KEY_NODE_FORCE_QUANTUM + PW_KEY_NODE_LOCK_QUANTUM the server should + // always call us with exactly effective_buffer_size frames. If it doesn't, + // skip the cycle rather than overrunning the DSP buffer or feeding the tick + // a mismatched block size. + if(nframes != static_cast(self.effective_buffer_size)) { - self.do_process(self.effective_buffer_size, current_time_ns); - nframes -= self.effective_buffer_size; - current_time_ns += double(self.effective_buffer_size) / self.effective_sample_rate; + ossia::logger().warn( + "PipeWire: unexpected block size {} (expected {}), skipping cycle", + nframes, self.effective_buffer_size); + return; } - if(nframes > 0) - { - self.do_process(self.effective_buffer_size, current_time_ns); - } + self.do_process(nframes, current_time_ns); } std::vector input_ports; diff --git a/src/ossia/dataflow/data_copy.hpp b/src/ossia/dataflow/data_copy.hpp index fb50f0f9f56..62d23cc16a6 100644 --- a/src/ossia/dataflow/data_copy.hpp +++ b/src/ossia/dataflow/data_copy.hpp @@ -188,39 +188,49 @@ struct copy_data_pos const std::size_t pos; template - void operator()(const T&, const U&) const + bool operator()(const T&, const U&) const { + return false; } - void operator()(const value_delay_line& out, value_port& in) + bool operator()(const value_delay_line& out, value_port& in) { if(pos < out.data.size()) { copy_data{}(out.data[pos], in); + return true; } + return false; } - void operator()(const audio_delay_line& out, audio_port& in) + bool operator()(const audio_delay_line& out, audio_port& in) { if(pos < out.samples.size()) { mix(out.samples[pos], in.get()); + return true; } + return false; } - void operator()(const midi_delay_line& out, midi_port& in) + bool operator()(const midi_delay_line& out, midi_port& in) { if(pos < out.messages.size()) { copy_data{}(out.messages[pos], in); + return true; } + return false; } - void operator()(const geometry_delay_line& out, geometry_port& in) + + bool operator()(const geometry_delay_line& out, geometry_port& in) { if(pos < out.geometries.size()) { copy_data{}(out.geometries[pos], in); + return true; } + return false; } }; } diff --git a/src/ossia/dataflow/geometry_port.cpp b/src/ossia/dataflow/geometry_port.cpp new file mode 100644 index 00000000000..566f761c209 --- /dev/null +++ b/src/ossia/dataflow/geometry_port.cpp @@ -0,0 +1,175 @@ +#include + +#include +#include + +namespace ossia +{ + +struct semantic_entry +{ + attribute_semantic semantic; + std::string_view name; +}; + +static bool icase_equal(std::string_view a, std::string_view b) noexcept +{ + if(a.size() != b.size()) + return false; + for(std::size_t i = 0; i < a.size(); ++i) + if(std::tolower(static_cast(a[i])) + != std::tolower(static_cast(b[i]))) + return false; + return true; +} + +// Sorted by enum value for binary search in semantic_to_name. +// Sorted by name for binary search in name_to_semantic (separate array below). +static constexpr std::array semantic_table = { + // Core geometry + semantic_entry{attribute_semantic::position, "position"}, + semantic_entry{attribute_semantic::normal, "normal"}, + semantic_entry{attribute_semantic::tangent, "tangent"}, + semantic_entry{attribute_semantic::bitangent, "bitangent"}, + + // Basic materials + semantic_entry{attribute_semantic::texcoord0, "texcoord0"}, + semantic_entry{attribute_semantic::texcoord0, "texcoord"}, // alias + semantic_entry{attribute_semantic::texcoord1, "texcoord1"}, + semantic_entry{attribute_semantic::texcoord2, "texcoord2"}, + semantic_entry{attribute_semantic::texcoord3, "texcoord3"}, + semantic_entry{attribute_semantic::texcoord4, "texcoord4"}, + semantic_entry{attribute_semantic::texcoord5, "texcoord5"}, + semantic_entry{attribute_semantic::texcoord6, "texcoord6"}, + semantic_entry{attribute_semantic::texcoord7, "texcoord7"}, + + semantic_entry{attribute_semantic::color0, "color0"}, + semantic_entry{attribute_semantic::color0, "color"}, // alias + semantic_entry{attribute_semantic::color1, "color1"}, + semantic_entry{attribute_semantic::color2, "color2"}, + semantic_entry{attribute_semantic::color3, "color3"}, + + // Skinning / skeletal animation + semantic_entry{attribute_semantic::joints0, "joints0"}, + semantic_entry{attribute_semantic::joints1, "joints1"}, + semantic_entry{attribute_semantic::weights0, "weights0"}, + semantic_entry{attribute_semantic::weights1, "weights1"}, + + // Morph targets / blend shapes + semantic_entry{attribute_semantic::morph_position, "morph_position"}, + semantic_entry{attribute_semantic::morph_normal, "morph_normal"}, + semantic_entry{attribute_semantic::morph_tangent, "morph_tangent"}, + semantic_entry{attribute_semantic::morph_texcoord, "morph_texcoord"}, + semantic_entry{attribute_semantic::morph_color, "morph_color"}, + + // Transform / instancing + semantic_entry{attribute_semantic::rotation, "rotation"}, + semantic_entry{attribute_semantic::rotation_extra, "rotation_extra"}, + semantic_entry{attribute_semantic::scale, "scale"}, + semantic_entry{attribute_semantic::uniform_scale, "uniform_scale"}, + semantic_entry{attribute_semantic::up, "up"}, + semantic_entry{attribute_semantic::pivot, "pivot"}, + semantic_entry{attribute_semantic::transform_matrix, "transform_matrix"}, + semantic_entry{attribute_semantic::translation, "translation"}, + + // Particle dynamics + semantic_entry{attribute_semantic::velocity, "velocity"}, + semantic_entry{attribute_semantic::acceleration, "acceleration"}, + semantic_entry{attribute_semantic::force, "force"}, + semantic_entry{attribute_semantic::mass, "mass"}, + semantic_entry{attribute_semantic::age, "age"}, + semantic_entry{attribute_semantic::lifetime, "lifetime"}, + semantic_entry{attribute_semantic::birth_time, "birth_time"}, + semantic_entry{attribute_semantic::particle_id, "particle_id"}, + semantic_entry{attribute_semantic::drag, "drag"}, + semantic_entry{attribute_semantic::angular_velocity, "angular_velocity"}, + semantic_entry{attribute_semantic::previous_position, "previous_position"}, + semantic_entry{attribute_semantic::rest_position, "rest_position"}, + semantic_entry{attribute_semantic::target_position, "target_position"}, + semantic_entry{attribute_semantic::previous_velocity, "previous_velocity"}, + semantic_entry{attribute_semantic::state, "state"}, + semantic_entry{attribute_semantic::collision_count, "collision_count"}, + semantic_entry{attribute_semantic::collision_normal, "collision_normal"}, + semantic_entry{attribute_semantic::sleep, "sleep"}, + + // Rendering hints + semantic_entry{attribute_semantic::sprite_size, "sprite_size"}, + semantic_entry{attribute_semantic::sprite_rotation, "sprite_rotation"}, + semantic_entry{attribute_semantic::sprite_facing, "sprite_facing"}, + semantic_entry{attribute_semantic::sprite_index, "sprite_index"}, + semantic_entry{attribute_semantic::width, "width"}, + semantic_entry{attribute_semantic::opacity, "opacity"}, + semantic_entry{attribute_semantic::emissive, "emissive"}, + semantic_entry{attribute_semantic::emissive_strength, "emissive_strength"}, + + // Material / PBR + semantic_entry{attribute_semantic::roughness, "roughness"}, + semantic_entry{attribute_semantic::metallic, "metallic"}, + semantic_entry{attribute_semantic::ambient_occlusion, "ambient_occlusion"}, + semantic_entry{attribute_semantic::specular, "specular"}, + semantic_entry{attribute_semantic::subsurface, "subsurface"}, + semantic_entry{attribute_semantic::clearcoat, "clearcoat"}, + semantic_entry{attribute_semantic::clearcoat_roughness, "clearcoat_roughness"}, + semantic_entry{attribute_semantic::anisotropy, "anisotropy"}, + semantic_entry{attribute_semantic::anisotropy_direction, "anisotropy_direction"}, + semantic_entry{attribute_semantic::ior, "ior"}, + semantic_entry{attribute_semantic::transmission, "transmission"}, + semantic_entry{attribute_semantic::thickness, "thickness"}, + semantic_entry{attribute_semantic::material_id, "material_id"}, + + // Gaussian splatting + semantic_entry{attribute_semantic::sh_dc, "sh_dc"}, + semantic_entry{attribute_semantic::sh_coeffs, "sh_coeffs"}, + semantic_entry{attribute_semantic::covariance_3d, "covariance_3d"}, + semantic_entry{attribute_semantic::sh_degree, "sh_degree"}, + + // Volumetric / field data + semantic_entry{attribute_semantic::density, "density"}, + semantic_entry{attribute_semantic::temperature, "temperature"}, + semantic_entry{attribute_semantic::fuel, "fuel"}, + semantic_entry{attribute_semantic::pressure, "pressure"}, + semantic_entry{attribute_semantic::divergence, "divergence"}, + semantic_entry{attribute_semantic::sdf_distance, "sdf_distance"}, + semantic_entry{attribute_semantic::voxel_color, "voxel_color"}, + + // Topology / connectivity + semantic_entry{attribute_semantic::name, "name"}, + semantic_entry{attribute_semantic::piece_id, "piece_id"}, + semantic_entry{attribute_semantic::line_id, "line_id"}, + semantic_entry{attribute_semantic::prim_id, "prim_id"}, + semantic_entry{attribute_semantic::point_id, "point_id"}, + semantic_entry{attribute_semantic::group_mask, "group_mask"}, + semantic_entry{attribute_semantic::instance_id, "instance_id"}, + + // UI + semantic_entry{attribute_semantic::selection, "selection"}, + + // User / general purpose + semantic_entry{attribute_semantic::fx0, "fx0"}, + semantic_entry{attribute_semantic::fx1, "fx1"}, + semantic_entry{attribute_semantic::fx2, "fx2"}, + semantic_entry{attribute_semantic::fx3, "fx3"}, + semantic_entry{attribute_semantic::fx4, "fx4"}, + semantic_entry{attribute_semantic::fx5, "fx5"}, + semantic_entry{attribute_semantic::fx6, "fx6"}, + semantic_entry{attribute_semantic::fx7, "fx7"}, +}; + +std::string_view semantic_to_name(attribute_semantic s) noexcept +{ + // FIXME lower_bound or boost::bimap if there's a constexpr one + for(auto& e : semantic_table) + if(e.semantic == s) + return e.name; + return {}; +} + +attribute_semantic name_to_semantic(std::string_view name) noexcept +{ + for(auto& e : semantic_table) + if(icase_equal(e.name, name)) + return e.semantic; + return attribute_semantic::custom; +} + +} diff --git a/src/ossia/dataflow/geometry_port.hpp b/src/ossia/dataflow/geometry_port.hpp index c7579339013..64b93b88544 100644 --- a/src/ossia/dataflow/geometry_port.hpp +++ b/src/ossia/dataflow/geometry_port.hpp @@ -4,11 +4,156 @@ #include #include +#include #include #include +#include namespace ossia { + +// clang-format off +// Semantic identification for geometry attributes. +// Custom attributes use attribute_semantic::custom + a string name. +enum class attribute_semantic : uint16_t +{ + // Core geometry + position = 0, // vec3. Object-space position. + normal = 1, // vec3. Surface normal. + tangent = 2, // vec4. xyz=tangent, w=handedness (±1). [glTF TANGENT] + bitangent = 3, // vec3. cross(N, T.xyz) * T.w. + + // Basic materials + texcoord0 = 100, // vec2/3. Primary UV. [glTF TEXCOORD_0] + texcoord1 = texcoord0 + 1, // vec2. Secondary UV (lightmaps). [glTF TEXCOORD_1] + texcoord2 = texcoord0 + 2, // vec2. Secondary UV. [glTF TEXCOORD_2] + texcoord3 = texcoord0 + 3, // vec2. Secondary UV. [glTF TEXCOORD_3] + texcoord4 = texcoord0 + 4, // vec2. Secondary UV. [glTF TEXCOORD_4] + texcoord5 = texcoord0 + 5, // vec2. Secondary UV. + texcoord6 = texcoord0 + 6, // vec2. Secondary UV. + texcoord7 = texcoord0 + 7, // vec2. Secondary UV. + + color0 = 200, // vec4. Vertex color RGBA. [glTF COLOR_0] + color1 = color0 + 1, // vec4. Secondary vertex color. [glTF COLOR_1] + color2 = color0 + 2, // vec4. Secondary vertex color. + color3 = color0 + 3, // vec4. Secondary vertex color. + + // Skinning / skeletal animation + joints0 = 300, // uvec4. Bone indices, set 0. [glTF JOINTS_0] + joints1 = joints0 + 1, // uvec4. Bone indices, set 1. [glTF JOINTS_1] + + weights0 = 400, // vec4. Bone weights, set 0. [glTF WEIGHTS_0] + weights1 = weights0 + 1, // vec4. Bone weights, set 1. [glTF WEIGHTS_1] + + // Morph targets / blend shapes + morph_position = 500, // vec3. Position delta for morph target. + morph_normal = morph_position + 1, // vec3. Normal delta for morph target. + morph_tangent = morph_position + 2, // vec3. Tangent delta (no w). [glTF morph TANGENT] + morph_texcoord = morph_position + 3, // vec2. UV delta for morph target. + morph_color = morph_position + 4, // vec3/4. Color delta for morph target. + + // Transform / instancing + rotation = 600, // vec4. Quaternion (x,y,z,w). + rotation_extra = morph_position + 1, // vec4. Post-orient rotation. + scale = morph_position + 2, // vec3. Non-uniform scale. + uniform_scale = morph_position + 3, // float. Uniform scale. + up = morph_position + 4, // vec3. Up vector for LookAt. + pivot = morph_position + 5, // vec3. Local pivot point. + transform_matrix = morph_position + 6, // mat4. Full transform, overrides TRS. (note: remember that mat4 takes 4 lanes of attributes) + translation = morph_position + 7, // vec3. Additional translation offset. + + // Particle dynamics + velocity = 1000, // vec3. Velocity in units/sec. + acceleration = velocity + 1, // vec3. Current acceleration. + force = velocity + 2, // vec3. Accumulated force this frame. + mass = velocity + 3, // float. + age = velocity + 4, // float. Time since birth, seconds. + lifetime = velocity + 5, // float. Max age before death. + birth_time = velocity + 6, // float. Absolute time of birth. + particle_id = velocity + 7, // int. Stable unique ID. + drag = velocity + 8, // float. Per-particle drag coefficient. + angular_velocity = velocity + 9, // vec3. Rotation speed rad/sec. + previous_position = velocity + 10, // vec3. For Verlet / motion blur. + rest_position = velocity + 11, // vec3. Undeformed position. + target_position = velocity + 12, // vec3. Goal position for constraints. + previous_velocity = velocity + 13, // vec3. Velocity at previous frame. + state = velocity + 14, // int. alive/dying/dead/collided enum. + collision_count = velocity + 15, // int. Number of collisions. + collision_normal = velocity + 16, // vec3. Normal at last collision. + sleep = velocity + 17, // int. Dormant flag (skip simulation). + + // Rendering hints + sprite_size = 1100, // vec2. Billboard width/height. + sprite_rotation = sprite_size + 1, // float. Billboard screen-space rotation. + sprite_facing = sprite_size + 2, // vec3. Custom billboard facing direction. + sprite_index = sprite_size + 3, // int/float. Sub-image index for sprite sheets. + width = sprite_size + 4, // float. Curve/ribbon thickness. + opacity = sprite_size + 5, // float. Separate from color alpha. + emissive = sprite_size + 6, // vec3. Self-illumination color. + emissive_strength = sprite_size + 7, // float. Emissive intensity multiplier. + + // Material / PBR + roughness = 1200, // float. PBR roughness [0-1]. + metallic = roughness + 1, // float. PBR metalness [0-1]. + ambient_occlusion = roughness + 2, // float. Baked AO [0-1]. + specular = roughness + 3, // float. Specular factor. + subsurface = roughness + 4, // float. SSS intensity. + clearcoat = roughness + 5, // float. Clearcoat factor. + clearcoat_roughness = roughness + 6, // float. Clearcoat roughness. + anisotropy = roughness + 7, // float. Anisotropic reflection. + anisotropy_direction = roughness + 8, // vec3. Anisotropy tangent direction. + ior = roughness + 9, // float. Index of refraction. + transmission = roughness + 10, // float. Transmission factor (glass-like). + thickness = roughness + 11, // float. Volume thickness for transmission. + material_id = roughness + 22, // int. Index into material array. + + // Gaussian splatting + sh_dc = 1300, // vec3. SH degree-0 (DC) color. + sh_coeffs = sh_dc + 1, // float[N]. SH coefficients for higher degrees. + covariance_3d = sh_dc + 2, // vec6 or mat3. 3D covariance (6 unique floats). + sh_degree = sh_dc + 3, // int. Active SH degree for this splat (0-3). + + // Volumetric / field data + density = 1400, // float. Scalar density. + temperature = density + 1, // float. + fuel = density + 2, // float. + pressure = density + 3, // float. + divergence = density + 4, // float. + sdf_distance = density + 5, // float. Signed distance field value. + voxel_color = density + 6, // vec4. Per-voxel RGBA. + + // Topology / connectivity + name = 1600, // string. Piece/group identifier. + piece_id = name + 1, // int. Numeric piece/group index. + line_id = name + 2, // int. Which line strip this point belongs to. + prim_id = name + 3, // int. Source primitive index. + point_id = name + 4, // int. Stable point ID (distinct from array index). + group_mask = name + 5, // int. Bitfield for group membership. + instance_id = name + 6, // int. Which instance this element belongs to. + + // UI + selection = 1700, // float. Soft selection weight [0-1]. + + // User / general purpose + fx0 = 2000, // float. General-purpose effect control. + fx1 = fx0 + 1, // float. General-purpose effect control. + fx2 = fx0 + 2, // float. General-purpose effect control. + fx3 = fx0 + 3, // float. General-purpose effect control. + fx4 = fx0 + 4, // float. General-purpose effect control. + fx5 = fx0 + 5, // float. General-purpose effect control. + fx6 = fx0 + 6, // float. General-purpose effect control. + fx7 = fx0 + 7, // float. General-purpose effect control. + + // Custom (string name lookup) + custom = 0xFFFF +}; + +// Returns a display name for well-known semantics, empty for custom. +OSSIA_EXPORT std::string_view semantic_to_name(attribute_semantic s) noexcept; + +// Returns the semantic for a well-known name, or custom if not recognized. +OSSIA_EXPORT attribute_semantic name_to_semantic(std::string_view name) noexcept; + struct geometry { struct cpu_buffer @@ -27,6 +172,7 @@ struct geometry { ossia::variant data; bool dirty{}; + int64_t active_element_count{-1}; // -1 = use full buffer; else first N elements valid }; struct binding @@ -78,6 +224,12 @@ struct geometry = float4; uint32_t byte_offset = 0; + + // Semantic identification for this attribute. + // For well-known semantics, name is empty (derivable from the enum). + // For custom semantics, name holds the user-defined attribute name. + attribute_semantic semantic = attribute_semantic::custom; + std::string name; }; struct input @@ -123,6 +275,67 @@ struct geometry uint32 } format{}; } index; + + // Axis-aligned bounding box. All zeros = not computed. + struct + { + float min[3]{}; + float max[3]{}; + } bounds; + + // Optional GPU buffer holding a uint32 element count for indirect dispatch/draw. + // When set, renderers can use drawIndirect/dispatchIndirect. + gpu_buffer indirect_count; + + // Auxiliary structured buffers that travel with the geometry. + // These are NOT per-vertex attributes; they are opaque buffers with + // application-defined internal layouts (e.g. particle bookkeeping structs, + // indirect dispatch/draw args, index lists). + // The buffer data lives in the `buffers` array; this struct references it. + // Consumers match by name against their shader storage declarations. + struct auxiliary_buffer + { + std::string name; // Shader-visible name for matching (e.g. "particle_aux") + int buffer{-1}; // Index into the buffers array + int64_t byte_offset{}; // Offset within the buffer + int64_t byte_size{}; // Size of the auxiliary data region + }; + ossia::small_vector auxiliary; + + // Find an auxiliary buffer by name. Returns nullptr if not found. + const auxiliary_buffer* find_auxiliary(std::string_view name) const noexcept + { + for(auto& a : auxiliary) + if(a.name == name) + return &a; + return nullptr; + } + + // Find an attribute by semantic enum. Returns nullptr if not found. + const attribute* find(attribute_semantic sem) const noexcept + { + for(auto& a : attributes) + if(a.semantic == sem) + return &a; + return nullptr; + } + + // Find a custom attribute by name. Returns nullptr if not found. + const attribute* find(std::string_view attr_name) const noexcept + { + for(auto& a : attributes) + if(a.semantic == attribute_semantic::custom && a.name == attr_name) + return &a; + return nullptr; + } + + // Returns the display name for an attribute (semantic name or custom name). + static std::string_view display_name(const attribute& a) noexcept + { + if(a.semantic != attribute_semantic::custom) + return semantic_to_name(a.semantic); + return a.name; + } }; struct mesh_list diff --git a/src/ossia/dataflow/graph/graph.hpp b/src/ossia/dataflow/graph/graph.hpp index 3e4fe7769fb..84884fe0a02 100644 --- a/src/ossia/dataflow/graph/graph.hpp +++ b/src/ossia/dataflow/graph/graph.hpp @@ -230,7 +230,8 @@ class OSSIA_EXPORT graph final const auto N = boost::num_vertices(impl); m_topo_order_cache.clear(); m_topo_order_cache.reserve(N); - boost::topological_sort(gr, std::back_inserter(m_topo_order_cache)); + auto view = boost::filtered_graph(gr, no_delay_edges{&gr}); + boost::topological_sort(view, std::back_inserter(m_topo_order_cache)); nodes.clear(); nodes.reserve(N); diff --git a/src/ossia/dataflow/graph/graph_ordering.hpp b/src/ossia/dataflow/graph/graph_ordering.hpp index a06cfd6c87f..60009d57a9c 100644 --- a/src/ossia/dataflow/graph/graph_ordering.hpp +++ b/src/ossia/dataflow/graph/graph_ordering.hpp @@ -77,7 +77,7 @@ struct init_node_visitor const graph_edge& edge; execution_state& e; - static void copy(const delay_line_type& out, std::size_t pos, inlet& in) + static bool copy(const delay_line_type& out, std::size_t pos, inlet& in) { const auto w = out.which(); if(w.to_std_index() == in.which() && w.valid()) @@ -85,27 +85,24 @@ struct init_node_visitor switch(w.index()) { case delay_line_type::index_of().index(): - copy_data_pos{pos}( + return copy_data_pos{pos}( *reinterpret_cast(out.target()), in.cast()); - break; case delay_line_type::index_of().index(): - copy_data_pos{pos}( + return copy_data_pos{pos}( *reinterpret_cast(out.target()), in.cast()); - break; case delay_line_type::index_of().index(): - copy_data_pos{pos}( + return copy_data_pos{pos}( *reinterpret_cast(out.target()), in.cast()); - break; case delay_line_type::index_of().index(): - copy_data_pos{pos}( + return copy_data_pos{pos}( *reinterpret_cast(out.target()), in.cast()); - break; } } + return false; } static void move(outlet& out, inlet& in) @@ -186,17 +183,15 @@ struct init_node_visitor bool operator()(delayed_glutton_connection& con) const { - // TODO If there is data... - // Else... - copy(con.buffer, con.pos, in); - con.pos++; + if(copy(con.buffer, con.pos, in)) + con.pos++; return false; } bool operator()(delayed_strict_connection& con) const { - copy(con.buffer, con.pos, in); - con.pos++; + if(copy(con.buffer, con.pos, in)) + con.pos++; return false; } diff --git a/src/ossia/dataflow/graph/graph_static.hpp b/src/ossia/dataflow/graph/graph_static.hpp index 18bfb96d462..0c8c8402b57 100644 --- a/src/ossia/dataflow/graph/graph_static.hpp +++ b/src/ossia/dataflow/graph/graph_static.hpp @@ -16,6 +16,8 @@ namespace ossia { +using filtered_graph_t = std::decay_t(), no_delay_edges{nullptr}))>; template struct graph_static final : public graph_util @@ -25,7 +27,7 @@ struct graph_static final UpdateImpl update_fun; TickImpl tick_fun{*this}; std::vector m_color_map_cache; - std::vector> m_stack_cache; + std::vector> m_stack_cache; explicit graph_static(const ossia::graph_setup_options& opt = {}) : update_fun{*this, opt} { @@ -50,8 +52,10 @@ struct graph_static final // TODO this should be doable with a single vector m_topo_order_cache.clear(); m_topo_order_cache.reserve(m_nodes.size()); - custom_topological_sort( - gr, std::back_inserter(m_topo_order_cache), m_color_map_cache, m_stack_cache); + auto view = boost::filtered_graph{gr, no_delay_edges{&gr}}; + custom_topological_sort( + view, std::back_inserter(m_topo_order_cache), m_color_map_cache, + m_stack_cache); // First put the ones without any I/O (most likely states) for(auto vtx : m_topo_order_cache) diff --git a/src/ossia/dataflow/graph/graph_utils.hpp b/src/ossia/dataflow/graph/graph_utils.hpp index 5619073e622..289ec53db05 100644 --- a/src/ossia/dataflow/graph/graph_utils.hpp +++ b/src/ossia/dataflow/graph/graph_utils.hpp @@ -18,6 +18,7 @@ #include #include +#include #include // broken due to dynamic_property_map requiring rtti... // #include @@ -96,19 +97,26 @@ namespace boost { namespace detail { + template +using VertexDescriptor = typename graph_traits::vertex_descriptor; +template +using OutEdgeIterator = std::decay_t( + out_edges(std::declval>(), std::declval())))>; + +template using DFSVertexInfo = std::pair< typename graph_traits::vertex_descriptor, std::pair< boost::optional::edge_descriptor>, - std::pair< - typename graph_traits::out_edge_iterator, - typename graph_traits::out_edge_iterator>>>; + std::pair, OutEdgeIterator>>>; -template +template < + typename IncidenceGraph, typename FilteredGraph, class DFSVisitor, class ColorMap> void custom_depth_first_visit_impl( - const IncidenceGraph& g, typename graph_traits::vertex_descriptor u, - DFSVisitor& vis, ColorMap color, std::vector>& stack) + const FilteredGraph& g, typename graph_traits::vertex_descriptor u, + DFSVisitor& vis, ColorMap color, + std::vector>& stack) { constexpr detail::nontruth2 func; BOOST_CONCEPT_ASSERT((IncidenceGraphConcept)); @@ -124,7 +132,6 @@ void custom_depth_first_visit_impl( VertexInfo; boost::optional src_e; - Iter ei, ei_end; // Possible optimization for vector stack.clear(); @@ -132,7 +139,8 @@ void custom_depth_first_visit_impl( put(color, u, Color::gray()); vis.discover_vertex(u, g); - boost::tie(ei, ei_end) = out_edges(u, g); + auto [ei, ei_end] = out_edges(u, g); + if(func(u, g)) { // If this vertex terminates the search, we push empty range @@ -146,7 +154,7 @@ void custom_depth_first_visit_impl( } while(!stack.empty()) { - VertexInfo& back = stack.back(); + auto& back = stack.back(); u = back.first; src_e = back.second.first; boost::tie(ei, ei_end) = back.second.second; @@ -197,19 +205,19 @@ void custom_depth_first_visit_impl( } } -template +template < + typename VertexListGraph, typename FilteredGraph, class DFSVisitor, class ColorMap> void custom_depth_first_search( - const VertexListGraph& g, DFSVisitor vis, ColorMap color, + const FilteredGraph& g, DFSVisitor vis, ColorMap color, typename graph_traits::vertex_descriptor start_vertex, - std::vector>& stack) + std::vector>& stack) { typedef typename graph_traits::vertex_descriptor Vertex; BOOST_CONCEPT_ASSERT((DFSVisitorConcept)); typedef typename property_traits::value_type ColorValue; typedef color_traits Color; - typename graph_traits::vertex_iterator ui, ui_end; - for(boost::tie(ui, ui_end) = vertices(g); ui != ui_end; ++ui) + for(auto [ui, ui_end] = vertices(g); ui != ui_end; ++ui) { Vertex u = implicit_cast(*ui); put(color, u, Color::white()); @@ -219,17 +227,18 @@ void custom_depth_first_search( if(start_vertex != detail::get_default_starting_vertex(g)) { vis.start_vertex(start_vertex, g); - detail::custom_depth_first_visit_impl(g, start_vertex, vis, color, stack); + detail::custom_depth_first_visit_impl( + g, start_vertex, vis, color, stack); } - for(boost::tie(ui, ui_end) = vertices(g); ui != ui_end; ++ui) + for(auto [ui, ui_end] = vertices(g); ui != ui_end; ++ui) { Vertex u = implicit_cast(*ui); ColorValue u_color = get(color, u); if(u_color == Color::white()) { vis.start_vertex(u, g); - detail::custom_depth_first_visit_impl(g, u, vis, color, stack); + detail::custom_depth_first_visit_impl(g, u, vis, color, stack); } } } @@ -244,11 +253,11 @@ inline void remove_vertex(typename graph_t::vertex_descriptor v, graph_t& g) boost::detail::remove_vertex_dispatch(g, v, Cat()); } -template +template void custom_topological_sort( - VertexListGraph& g, OutputIterator result, + FilteredGraph& g, OutputIterator result, std::vector& color_map, - std::vector>& stack) + std::vector>& stack) { color_map.clear(); color_map.resize(boost::num_vertices(g)); @@ -256,7 +265,7 @@ void custom_topological_sort( auto map = boost::make_iterator_property_map( color_map.begin(), boost::get(boost::vertex_index, g), color_map[0]); - boost::custom_depth_first_search( + boost::custom_depth_first_search( g, boost::topo_sort_visitor(result), map, boost::detail::get_default_starting_vertex(g), stack); @@ -364,6 +373,24 @@ void print_graph(Graph_T& g, IO& stream) #endif } +struct no_delay_edges +{ + const graph_t* g{}; + + bool operator()(const boost::graph_traits::edge_descriptor& e) const + { + switch((*g)[e]->con.index()) + { + case 0: + case 1: + case 4: + return true; + default: + return false; + } + } +}; + struct OSSIA_EXPORT graph_util { static void pull_from_parameter(inlet& in, execution_state& e) @@ -669,7 +696,8 @@ struct OSSIA_EXPORT graph_util bool previous_nodes_executed = ossia::all_of(inlet.sources, [&](graph_edge* edge) { return edge->out_node->executed() || (!edge->out_node->enabled() /* && bool(inlet->address) */ - /* TODO check that it's in scope */); + /* TODO check that it's in scope */) + || edge->delayed(); }); // it does not have source ports ; we have to check for variables : @@ -837,6 +865,7 @@ struct OSSIA_EXPORT graph_base : graph_interface // TODO check that two edges can be added boost::add_edge(in_vtx, out_vtx, edge, m_graph); + recompute_maps(); m_dirty = true; } diff --git a/src/ossia/dataflow/graph_edge.hpp b/src/ossia/dataflow/graph_edge.hpp index 2a058e204a8..43ef7fd95df 100644 --- a/src/ossia/dataflow/graph_edge.hpp +++ b/src/ossia/dataflow/graph_edge.hpp @@ -42,6 +42,21 @@ struct OSSIA_EXPORT graph_edge void init() noexcept; void clear() noexcept; + bool delayed() const noexcept + { + switch(con.index()) + { + default: + case 0: + case 1: + case 4: + return false; + case 2: + case 3: + return true; + } + } + static std::size_t size_of_allocated_memory_by_make_shared() noexcept; connection con{}; diff --git a/src/ossia/dataflow/texture_port.hpp b/src/ossia/dataflow/texture_port.hpp index 779d3280067..06f5f73364f 100644 --- a/src/ossia/dataflow/texture_port.hpp +++ b/src/ossia/dataflow/texture_port.hpp @@ -71,6 +71,12 @@ struct render_target_spec struct buffer_spec { - + enum class usage : uint8_t + { + direct = 0, + indirect_draw = 1, + indirect_draw_indexed = 2 + }; + usage buffer_usage{usage::direct}; }; } diff --git a/src/ossia_features.cmake b/src/ossia_features.cmake index a7c3e7dde30..a5bf65e2f6c 100644 --- a/src/ossia_features.cmake +++ b/src/ossia_features.cmake @@ -115,15 +115,9 @@ if(OSSIA_PROTOCOL_WEBSOCKETS) endif() if(OSSIA_PROTOCOL_SERIAL) - find_package(${QT_VERSION} QUIET COMPONENTS Core SerialPort) - if(TARGET "${QT_PREFIX}::SerialPort") - target_sources(ossia PRIVATE ${OSSIA_SERIAL_HEADERS} ${OSSIA_SERIAL_SRCS}) - target_link_libraries(ossia PUBLIC ${QT_PREFIX}::SerialPort) - set(OSSIA_PROTOCOLS ${OSSIA_PROTOCOLS} Serial) - set(OSSIA_PROTOCOL_SERIAL TRUE CACHE INTERNAL "" FORCE) - else() - set(OSSIA_PROTOCOL_SERIAL FALSE CACHE INTERNAL "") - endif() + target_sources(ossia PRIVATE ${OSSIA_SERIAL_HEADERS} ${OSSIA_SERIAL_SRCS}) + set(OSSIA_PROTOCOLS ${OSSIA_PROTOCOLS} Serial) + set(OSSIA_PROTOCOL_SERIAL TRUE CACHE INTERNAL "" FORCE) endif() if(OSSIA_PROTOCOL_PHIDGETS) diff --git a/src/ossia_sources.cmake b/src/ossia_sources.cmake index 541a900ad80..b11b0fc0ca3 100644 --- a/src/ossia_sources.cmake +++ b/src/ossia_sources.cmake @@ -858,6 +858,7 @@ set(OSSIA_DATAFLOW_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/execution/ordered_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/execution/priorized_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/data.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/geometry_port.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/port.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/graph_node.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ossia/dataflow/execution_state.cpp"