#pragma once //////////////////////////////////////////////////////////////////////////////// // The MIT License (MIT) // // Copyright (c) 2017 Nicholas Frechette & Animation Compression Library contributors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. //////////////////////////////////////////////////////////////////////////////// #include "acl/core/impl/compiler_utils.h" #include "acl/core/iallocator.h" #include "acl/core/error.h" #include "acl/core/track_formats.h" #include "acl/core/variable_bit_rates.h" #include "acl/compression/impl/animated_track_utils.h" #include "acl/compression/impl/clip_context.h" #include "acl/core/impl/compressed_headers.h" #include ACL_IMPL_FILE_PRAGMA_PUSH namespace acl { namespace acl_impl { inline uint32_t get_constant_data_size(const clip_context& clip) { // Only use the first segment, it contains the necessary information const SegmentContext& segment = clip.segments[0]; uint32_t constant_data_size = 0; for (uint32_t bone_index = 0; bone_index < clip.num_bones; ++bone_index) { const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (bone_stream.output_index == k_invalid_track_index) continue; // Stripped if (!bone_stream.is_rotation_default && bone_stream.is_rotation_constant) constant_data_size += bone_stream.rotations.get_packed_sample_size(); if (!bone_stream.is_translation_default && bone_stream.is_translation_constant) constant_data_size += bone_stream.translations.get_packed_sample_size(); if (clip.has_scale && !bone_stream.is_scale_default && bone_stream.is_scale_constant) constant_data_size += bone_stream.scales.get_packed_sample_size(); } return constant_data_size; } inline void get_num_constant_samples(const clip_context& clip, uint32_t& out_num_constant_rotation_samples, uint32_t& out_num_constant_translation_samples, uint32_t& out_num_constant_scale_samples) { uint32_t num_constant_rotation_samples = 0; uint32_t num_constant_translation_samples = 0; uint32_t num_constant_scale_samples = 0; // Only use the first segment, it contains the necessary information const SegmentContext& segment = clip.segments[0]; for (uint32_t bone_index = 0; bone_index < clip.num_bones; ++bone_index) { const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (bone_stream.output_index == k_invalid_track_index) continue; // Stripped if (!bone_stream.is_rotation_default && bone_stream.is_rotation_constant) num_constant_rotation_samples++; if (!bone_stream.is_translation_default && bone_stream.is_translation_constant) num_constant_translation_samples++; if (clip.has_scale && !bone_stream.is_scale_default && bone_stream.is_scale_constant) num_constant_scale_samples++; } out_num_constant_rotation_samples = num_constant_rotation_samples; out_num_constant_translation_samples = num_constant_translation_samples; out_num_constant_scale_samples = num_constant_scale_samples; } inline void get_animated_variable_bit_rate_data_size(const TrackStream& track_stream, uint32_t num_samples, uint32_t& out_num_animated_data_bits, uint32_t& out_num_animated_pose_bits) { const uint8_t bit_rate = track_stream.get_bit_rate(); const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3; // 3 components out_num_animated_data_bits += num_bits_at_bit_rate * num_samples; out_num_animated_pose_bits += num_bits_at_bit_rate; } inline void calculate_animated_data_size(const TrackStream& track_stream, uint32_t& num_animated_data_bits, uint32_t& num_animated_pose_bits) { const uint32_t num_samples = track_stream.get_num_samples(); if (track_stream.is_bit_rate_variable()) { get_animated_variable_bit_rate_data_size(track_stream, num_samples, num_animated_data_bits, num_animated_pose_bits); } else { const uint32_t sample_size = track_stream.get_packed_sample_size(); num_animated_data_bits += sample_size * num_samples * 8; num_animated_pose_bits += sample_size * 8; } } inline void calculate_animated_data_size(clip_context& clip, const uint32_t* output_bone_mapping, uint32_t num_output_bones) { for (SegmentContext& segment : clip.segment_iterator()) { uint32_t num_animated_rotation_data_bits = 0; uint32_t num_animated_translation_data_bits = 0; uint32_t num_animated_scale_data_bits = 0; uint32_t num_animated_pose_bits = 0; for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index) { const uint32_t bone_index = output_bone_mapping[output_index]; const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (!bone_stream.is_rotation_constant) calculate_animated_data_size(bone_stream.rotations, num_animated_rotation_data_bits, num_animated_pose_bits); if (!bone_stream.is_translation_constant) calculate_animated_data_size(bone_stream.translations, num_animated_translation_data_bits, num_animated_pose_bits); if (!bone_stream.is_scale_constant) calculate_animated_data_size(bone_stream.scales, num_animated_scale_data_bits, num_animated_pose_bits); } const uint32_t num_animated_data_bits = num_animated_rotation_data_bits + num_animated_translation_data_bits + num_animated_scale_data_bits; segment.animated_pose_rotation_bit_size = num_animated_rotation_data_bits; segment.animated_pose_translation_bit_size = num_animated_translation_data_bits; segment.animated_pose_scale_bit_size = num_animated_scale_data_bits; segment.animated_data_size = align_to(num_animated_data_bits, 8) / 8; segment.animated_pose_bit_size = num_animated_pose_bits; } } inline uint32_t get_format_per_track_data_size(const clip_context& clip, rotation_format8 rotation_format, vector_format8 translation_format, vector_format8 scale_format, uint32_t* out_num_animated_variable_sub_tracks_padded = nullptr) { const bool is_rotation_variable = is_rotation_format_variable(rotation_format); const bool is_translation_variable = is_vector_format_variable(translation_format); const bool is_scale_variable = is_vector_format_variable(scale_format); // Only use the first segment, it contains the necessary information const SegmentContext& segment = clip.segments[0]; uint32_t format_per_track_data_size = 0; uint32_t num_animated_variable_rotations = 0; for (const BoneStreams& bone_stream : segment.const_bone_iterator()) { if (bone_stream.is_stripped_from_output()) continue; if (!bone_stream.is_rotation_constant && is_rotation_variable) { format_per_track_data_size++; num_animated_variable_rotations++; } if (!bone_stream.is_translation_constant && is_translation_variable) format_per_track_data_size++; if (!bone_stream.is_scale_constant && is_scale_variable) format_per_track_data_size++; } // Rotations are padded for alignment const uint32_t num_partial_rotations = num_animated_variable_rotations % 4; if (num_partial_rotations != 0) format_per_track_data_size += 4 - num_partial_rotations; if (out_num_animated_variable_sub_tracks_padded != nullptr) *out_num_animated_variable_sub_tracks_padded = format_per_track_data_size; // 1 byte per sub-track return format_per_track_data_size; } inline uint32_t write_constant_track_data(const clip_context& clip, rotation_format8 rotation_format, uint8_t* constant_data, uint32_t constant_data_size, const uint32_t* output_bone_mapping, uint32_t num_output_bones) { ACL_ASSERT(constant_data != nullptr, "'constant_data' cannot be null!"); (void)constant_data_size; // Only use the first segment, it contains the necessary information const SegmentContext& segment = clip.segments[0]; #if defined(ACL_HAS_ASSERT_CHECKS) const uint8_t* constant_data_end = add_offset_to_ptr(constant_data, constant_data_size); #endif const uint8_t* constant_data_start = constant_data; #if defined(ACL_IMPL_USE_CONSTANT_GROUPS) // Data is ordered in groups of 4 constant sub-tracks (e.g rot0, rot1, rot2, rot3) // Order depends on animated track order. If we have 6 constant rotation tracks before the first constant // translation track, we'll have 8 constant rotation sub-tracks followed by 4 constant translation sub-tracks. // Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks. // This is because we always process 4 constant sub-tracks at a time and cache the results. // Groups are written in the order of first use and as such are sorted by their lowest sub-track index. // If our rotation format drops the W component, we swizzle the data to store XXXX, YYYY, ZZZZ const bool swizzle_rotations = get_rotation_variant(rotation_format) == rotation_variant8::quat_drop_w; float xxxx_group[4]; float yyyy_group[4]; float zzzz_group[4]; rtm::vector4f constant_group4[4]; rtm::float3f constant_group3[4]; auto group_entry_action = [&](animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index) { const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (group_type == animation_track_type8::rotation) { if (swizzle_rotations) { const rtm::vector4f sample = bone_stream.rotations.get_raw_sample(0); xxxx_group[group_size] = rtm::vector_get_x(sample); yyyy_group[group_size] = rtm::vector_get_y(sample); zzzz_group[group_size] = rtm::vector_get_z(sample); } else { const rtm::vector4f sample = bone_stream.rotations.get_raw_sample(0); constant_group4[group_size] = sample; } } else if (group_type == animation_track_type8::translation) { const rtm::vector4f sample = bone_stream.translations.get_raw_sample(0); rtm::vector_store3(sample, &constant_group3[group_size]); } else { const rtm::vector4f sample = bone_stream.scales.get_raw_sample(0); rtm::vector_store3(sample, &constant_group3[group_size]); } }; auto group_flush_action = [&](animation_track_type8 group_type, uint32_t group_size) { if (group_type == animation_track_type8::rotation) { if (swizzle_rotations) { std::memcpy(constant_data, &xxxx_group[0], group_size * sizeof(float)); constant_data += group_size * sizeof(float); std::memcpy(constant_data, &yyyy_group[0], group_size * sizeof(float)); constant_data += group_size * sizeof(float); std::memcpy(constant_data, &zzzz_group[0], group_size * sizeof(float)); constant_data += group_size * sizeof(float); } else { // If we don't swizzle, we have a full quaternion std::memcpy(constant_data, &constant_group4[0], group_size * sizeof(rtm::vector4f)); constant_data += group_size * sizeof(rtm::vector4f); } } else { std::memcpy(constant_data, &constant_group3[0], group_size * sizeof(rtm::float3f)); constant_data += group_size * sizeof(rtm::float3f); } ACL_ASSERT(constant_data <= constant_data_end, "Invalid constant data offset. Wrote too much data."); }; constant_group_writer(segment, output_bone_mapping, num_output_bones, group_entry_action, group_flush_action); #else // If our rotation format drops the W component, we swizzle the data to store XXXX, YYYY, ZZZZ const bool swizzle_rotations = get_rotation_variant(rotation_format) == rotation_variant8::quat_drop_w; float xxxx[4]; float yyyy[4]; float zzzz[4]; uint32_t num_swizzle_written = 0; // Write rotations first for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index) { const uint32_t bone_index = output_bone_mapping[output_index]; const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (!bone_stream.is_rotation_default && bone_stream.is_rotation_constant) { if (swizzle_rotations) { const rtm::vector4f rotation = bone_stream.rotations.get_raw_sample(0); xxxx[num_swizzle_written] = rtm::vector_get_x(rotation); yyyy[num_swizzle_written] = rtm::vector_get_y(rotation); zzzz[num_swizzle_written] = rtm::vector_get_z(rotation); num_swizzle_written++; if (num_swizzle_written >= 4) { std::memcpy(constant_data, &xxxx[0], sizeof(xxxx)); constant_data += sizeof(xxxx); std::memcpy(constant_data, &yyyy[0], sizeof(yyyy)); constant_data += sizeof(yyyy); std::memcpy(constant_data, &zzzz[0], sizeof(zzzz)); constant_data += sizeof(zzzz); num_swizzle_written = 0; } } else { const uint8_t* rotation_ptr = bone_stream.rotations.get_raw_sample_ptr(0); uint32_t sample_size = bone_stream.rotations.get_sample_size(); std::memcpy(constant_data, rotation_ptr, sample_size); constant_data += sample_size; } ACL_ASSERT(constant_data <= constant_data_end, "Invalid constant data offset. Wrote too much data."); } } if (swizzle_rotations && num_swizzle_written != 0) { std::memcpy(constant_data, &xxxx[0], num_swizzle_written * sizeof(float)); constant_data += num_swizzle_written * sizeof(float); std::memcpy(constant_data, &yyyy[0], num_swizzle_written * sizeof(float)); constant_data += num_swizzle_written * sizeof(float); std::memcpy(constant_data, &zzzz[0], num_swizzle_written * sizeof(float)); constant_data += num_swizzle_written * sizeof(float); ACL_ASSERT(constant_data <= constant_data_end, "Invalid constant data offset. Wrote too much data."); } // Next, write translations for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index) { const uint32_t bone_index = output_bone_mapping[output_index]; const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (!bone_stream.is_translation_default && bone_stream.is_translation_constant) { const uint8_t* translation_ptr = bone_stream.translations.get_raw_sample_ptr(0); uint32_t sample_size = bone_stream.translations.get_sample_size(); std::memcpy(constant_data, translation_ptr, sample_size); constant_data += sample_size; ACL_ASSERT(constant_data <= constant_data_end, "Invalid constant data offset. Wrote too much data."); } } // Finally, write scales if (clip.has_scale) { for (uint32_t output_index = 0; output_index < num_output_bones; ++output_index) { const uint32_t bone_index = output_bone_mapping[output_index]; const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (!bone_stream.is_scale_default && bone_stream.is_scale_constant) { const uint8_t* scale_ptr = bone_stream.scales.get_raw_sample_ptr(0); uint32_t sample_size = bone_stream.scales.get_sample_size(); std::memcpy(constant_data, scale_ptr, sample_size); constant_data += sample_size; ACL_ASSERT(constant_data <= constant_data_end, "Invalid constant data offset. Wrote too much data."); } } } #endif ACL_ASSERT(constant_data == constant_data_end, "Invalid constant data offset. Wrote too little data."); return safe_static_cast(constant_data - constant_data_start); } inline void write_animated_track_data(const TrackStream& track_stream, uint32_t sample_index, uint8_t* animated_track_data_begin, uint8_t*& out_animated_track_data, uint64_t& out_bit_offset) { const uint8_t* raw_sample_ptr = track_stream.get_raw_sample_ptr(sample_index); if (track_stream.is_bit_rate_variable()) { const uint8_t bit_rate = track_stream.get_bit_rate(); const uint64_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate) * 3; // 3 components // Track is constant, our constant sample is stored in the range information ACL_ASSERT(!is_constant_bit_rate(bit_rate), "Cannot write constant variable track data"); if (is_raw_bit_rate(bit_rate)) { const uint32_t* raw_sample_u32 = safe_ptr_cast(raw_sample_ptr); const uint32_t x = byte_swap(raw_sample_u32[0]); memcpy_bits(animated_track_data_begin, out_bit_offset + 0, &x, 0, 32); const uint32_t y = byte_swap(raw_sample_u32[1]); memcpy_bits(animated_track_data_begin, out_bit_offset + 32, &y, 0, 32); const uint32_t z = byte_swap(raw_sample_u32[2]); memcpy_bits(animated_track_data_begin, out_bit_offset + 64, &z, 0, 32); } else { const uint64_t raw_sample_u64 = *safe_ptr_cast(raw_sample_ptr); memcpy_bits(animated_track_data_begin, out_bit_offset, &raw_sample_u64, 0, num_bits_at_bit_rate); } out_bit_offset += num_bits_at_bit_rate; out_animated_track_data = animated_track_data_begin + (out_bit_offset / 8); } else { const uint32_t* raw_sample_u32 = safe_ptr_cast(raw_sample_ptr); const uint32_t x = byte_swap(raw_sample_u32[0]); memcpy_bits(animated_track_data_begin, out_bit_offset + 0, &x, 0, 32); const uint32_t y = byte_swap(raw_sample_u32[1]); memcpy_bits(animated_track_data_begin, out_bit_offset + 32, &y, 0, 32); const uint32_t z = byte_swap(raw_sample_u32[2]); memcpy_bits(animated_track_data_begin, out_bit_offset + 64, &z, 0, 32); const uint32_t sample_size = track_stream.get_packed_sample_size(); const bool has_w_component = sample_size == (sizeof(float) * 4); if (has_w_component) { const uint32_t w = byte_swap(raw_sample_u32[3]); memcpy_bits(animated_track_data_begin, out_bit_offset + 96, &w, 0, 32); } out_bit_offset += has_w_component ? 128 : 96; out_animated_track_data = animated_track_data_begin + (out_bit_offset / 8); } } inline uint32_t write_animated_track_data(const SegmentContext& segment, uint8_t* animated_track_data, uint32_t animated_data_size, const uint32_t* output_bone_mapping, uint32_t num_output_bones) { ACL_ASSERT(animated_track_data != nullptr, "'animated_track_data' cannot be null!"); (void)animated_data_size; uint8_t* animated_track_data_begin = animated_track_data; #if defined(ACL_HAS_ASSERT_CHECKS) const uint8_t* animated_track_data_end = add_offset_to_ptr(animated_track_data, animated_data_size); #endif const uint8_t* animated_track_data_start = animated_track_data; uint64_t bit_offset = 0; // Data is sorted first by time, second by bone. // This ensures that all bones are contiguous in memory when we sample a particular time. // Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3) // Order depends on animated track order. If we have 6 animated rotation tracks before the first animated // translation track, we'll have 8 animated rotation sub-tracks followed by 4 animated translation sub-tracks. // Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks. // This is because we always process 4 animated sub-tracks at a time and cache the results. // Groups are written in the order of first use and as such are sorted by their lowest sub-track index. // For animated samples, when we have a constant bit rate (bit rate 0), we do not store samples // and as such the group that contains that sub-track won't contain 4 samples. // The largest sample is a full precision vector4f, we can contain at most 4 samples alignas(16) uint8_t group_animated_track_data[sizeof(rtm::vector4f) * 4]; uint64_t group_bit_offset = 0; uint32_t num_group_samples = 0; uint8_t* dummy_animated_track_data_ptr = nullptr; auto group_filter_action = [&](animation_track_type8 group_type, uint32_t bone_index) { (void)group_type; (void)bone_index; // We want a group of every animated track // If a track is variable with a constant bit rate (bit rate 0), the group will have fewer entries return true; }; auto group_flush_action = [&](animation_track_type8 group_type, uint32_t group_size) { (void)group_type; if (group_size == 0) return; // Empty group, skip memcpy_bits(animated_track_data_begin, bit_offset, &group_animated_track_data[0], 0, group_bit_offset); bit_offset += group_bit_offset; group_bit_offset = 0; num_group_samples = 0; animated_track_data = animated_track_data_begin + (bit_offset / 8); ACL_ASSERT(animated_track_data <= animated_track_data_end, "Invalid animated track data offset. Wrote too much data."); }; // TODO: Use a group writer context object to avoid alloc/free/work in loop for every sample when it doesn't change for (uint32_t sample_index = 0; sample_index < segment.num_samples; ++sample_index) { auto group_entry_action = [&](animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index) { (void)group_size; const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (group_type == animation_track_type8::rotation) { if (!is_constant_bit_rate(bone_stream.rotations.get_bit_rate())) { write_animated_track_data(bone_stream.rotations, sample_index, group_animated_track_data, dummy_animated_track_data_ptr, group_bit_offset); num_group_samples++; } } else if (group_type == animation_track_type8::translation) { if (!is_constant_bit_rate(bone_stream.translations.get_bit_rate())) { write_animated_track_data(bone_stream.translations, sample_index, group_animated_track_data, dummy_animated_track_data_ptr, group_bit_offset); num_group_samples++; } } else { if (!is_constant_bit_rate(bone_stream.scales.get_bit_rate())) { write_animated_track_data(bone_stream.scales, sample_index, group_animated_track_data, dummy_animated_track_data_ptr, group_bit_offset); num_group_samples++; } } }; animated_group_writer(segment, output_bone_mapping, num_output_bones, group_filter_action, group_entry_action, group_flush_action); } if (bit_offset != 0) animated_track_data = animated_track_data_begin + (align_to(bit_offset, 8) / 8); ACL_ASSERT((bit_offset / segment.num_samples) == segment.animated_pose_bit_size, "Unexpected number of bits written"); ACL_ASSERT(animated_track_data == animated_track_data_end, "Invalid animated track data offset. Wrote too little data."); return safe_static_cast(animated_track_data - animated_track_data_start); } inline uint32_t write_format_per_track_data(const SegmentContext& segment, uint8_t* format_per_track_data, uint32_t format_per_track_data_size, const uint32_t* output_bone_mapping, uint32_t num_output_bones) { ACL_ASSERT(format_per_track_data != nullptr, "'format_per_track_data' cannot be null!"); (void)format_per_track_data_size; #if defined(ACL_HAS_ASSERT_CHECKS) const uint8_t* format_per_track_data_end = add_offset_to_ptr(format_per_track_data, format_per_track_data_size); #endif const uint8_t* format_per_track_data_start = format_per_track_data; // Data is ordered in groups of 4 animated sub-tracks (e.g rot0, rot1, rot2, rot3) // Order depends on animated track order. If we have 6 animated rotation tracks before the first animated // translation track, we'll have 8 animated rotation sub-tracks followed by 4 animated translation sub-tracks. // Once we reach the end, there is no extra padding. The last group might be less than 4 sub-tracks. // This is because we always process 4 animated sub-tracks at a time and cache the results. // Groups are written in the order of first use and as such are sorted by their lowest sub-track index. // To keep decompression simpler, rotations are padded to 4 elements even if the last group is partial uint8_t format_per_track_group[4]; auto group_filter_action = [&](animation_track_type8 group_type, uint32_t bone_index) { const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (group_type == animation_track_type8::rotation) return bone_stream.rotations.is_bit_rate_variable(); else if (group_type == animation_track_type8::translation) return bone_stream.translations.is_bit_rate_variable(); else return bone_stream.scales.is_bit_rate_variable(); }; auto group_entry_action = [&](animation_track_type8 group_type, uint32_t group_size, uint32_t bone_index) { const BoneStreams& bone_stream = segment.bone_streams[bone_index]; if (group_type == animation_track_type8::rotation) format_per_track_group[group_size] = (uint8_t)get_num_bits_at_bit_rate(bone_stream.rotations.get_bit_rate()); else if (group_type == animation_track_type8::translation) format_per_track_group[group_size] = (uint8_t)get_num_bits_at_bit_rate(bone_stream.translations.get_bit_rate()); else format_per_track_group[group_size] = (uint8_t)get_num_bits_at_bit_rate(bone_stream.scales.get_bit_rate()); }; auto group_flush_action = [&](animation_track_type8 group_type, uint32_t group_size) { const uint32_t copy_size = group_type == animation_track_type8::rotation ? 4 : group_size; std::memcpy(format_per_track_data, &format_per_track_group[0], copy_size); format_per_track_data += copy_size; // Zero out the temporary buffer for the final group to not contain partial garbage std::memset(&format_per_track_group[0], 0, sizeof(format_per_track_group)); ACL_ASSERT(format_per_track_data <= format_per_track_data_end, "Invalid format per track data offset. Wrote too much data."); }; animated_group_writer(segment, output_bone_mapping, num_output_bones, group_filter_action, group_entry_action, group_flush_action); ACL_ASSERT(format_per_track_data == format_per_track_data_end, "Invalid format per track data offset. Wrote too little data."); return safe_static_cast(format_per_track_data - format_per_track_data_start); } inline uint32_t write_animated_group_types(const animation_track_type8* animated_sub_track_groups, uint32_t num_animated_groups, animation_track_type8* animated_sub_track_groups_data, uint32_t animated_sub_track_groups_data_size) { (void)animated_sub_track_groups_data_size; const animation_track_type8* animated_sub_track_groups_data_start = animated_sub_track_groups_data; std::memcpy(animated_sub_track_groups_data, animated_sub_track_groups, sizeof(animation_track_type8) * num_animated_groups); animated_sub_track_groups_data += num_animated_groups; animated_sub_track_groups_data[0] = static_cast(0xFF); // Terminator animated_sub_track_groups_data++; ACL_ASSERT(animated_sub_track_groups_data == animated_sub_track_groups_data_start + animated_sub_track_groups_data_size, "Too little or too much data written"); return static_cast(animated_sub_track_groups_data - animated_sub_track_groups_data_start); } } } ACL_IMPL_FILE_PRAGMA_POP