Skip to content

Conversation

@shamilatesoglu
Copy link
Member

This node adds a generic frame-rate conversion capability between producer and consumer threads.

{
std::unique_lock lock(Mutex);
Tail = (Tail + count) % Capacity;
Size = (Size >= count) ? (Size - count) : 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

soft check on this case being false?


bool CapacityUpdatedViaPathCommand = false;

FrameRateConverterNode() : Ring(1, RingBufferServeMode::WaitUntilFull)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This node's serve mode should be either dynamic(via pin or template parameter on nosnode) or we can make two separate nodes

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new Frame Rate Converter node that enables generic frame-rate conversion between producer and consumer threads. The implementation refactors the existing RingBuffer infrastructure to support runtime mode selection and adds multi-element push/pop operations.

Key changes:

  • New FrameRateConverter node that can convert between different frame rates using a configurable ratio
  • Refactoring of RingBuffer from compile-time template-based mode selection to runtime-based mode configuration
  • Addition of multi-element push/pop operations (BeginPushMultiple, EndPushMultiple, BeginPopMultiple, EndPopMultiple) to support batch operations

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
Plugins/nosReflect/Types/Reflect.fbs Adds RingBufferServeMode enum to replace compile-time template parameter
Plugins/nosReflect/Source/RingBuffer.hpp Refactors RingBuffer to use runtime mode selection and adds multi-element push/pop methods; migrates pin watching from OnPinObjectChanged to AddPinValueWatcher
Plugins/nosReflect/Source/RingBufferNodes.cpp Updates node definitions to use explicit constructors with runtime mode specification
Plugins/nosReflect/Source/BoundedQueueNodes.cpp Updates node definitions to use explicit constructors with ServeImmediately mode
Plugins/nosReflect/Source/FrameRateConverter.cpp Implements new FrameRateConverter node with ratio-based frame rate conversion using multi-element ring buffer operations
Plugins/nosReflect/Source/PluginMain.cpp Registers the new FrameRateConverter node
Plugins/nosReflect/Nodes/FrameRateConverter.nosnode Defines the FrameRateConverter node configuration with Capacity and Ratio pins

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

{
State = RingState::Serving;
ReadyForPopCV.notify_all();
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notification logic after EndPushMultiple has changed from the previous implementation. Previously, when Mode was ServeImmediately (old WaitUntilFull == false), consumers were notified on every push. Now, consumers are only notified when transitioning from Filling to Serving state. This means in ServeImmediately mode, consumers won't be notified when new data becomes available, potentially causing them to wait until timeout. Add notification when State is already Serving to maintain the previous behavior.

Suggested change
}
}
else if (State == RingState::Serving)
{
// In ServeImmediately mode (or once already serving), notify consumers on each push
ReadyForPopCV.notify_all();
}

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 27
explicit RingBuffer(size_t capacity, RingBufferServeMode mode = RingBufferServeMode::WaitUntilFull)
: Capacity(capacity),
Buffer(capacity),
Buffer(),
Head(0),
Tail(0),
Size(0),
ExitRequested(false)
{
Reset(capacity, mode);
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Reset method should initialize the Mode member before the constructor body is reached. Currently, the constructor calls Reset(capacity, mode) at line 26, but the Mode member is already initialized with a default value at line 234. The Reset call should properly set Mode, but it's cleaner to initialize Mode in the initializer list using the mode parameter directly.

Copilot uses AI. Check for mistakes.
if (State == RingState::Filling && Size == Capacity)
{
State = RingState::Serving;
ReadyForPopCV.notify_all();
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notification strategy after EndPushMultiple should be consistent with EndPush. EndPush uses notify_one() when transitioning to Serving state (line 73), but EndPushMultiple uses notify_all() (line 85). This inconsistency could lead to unnecessary wake-ups. Consider using notify_one() here for consistency unless there's a specific reason multiple waiters need to be notified.

Suggested change
ReadyForPopCV.notify_all();
ReadyForPopCV.notify_one();

Copilot uses AI. Check for mistakes.
Tail = (Tail + count) % Capacity;
Size = (Size >= count) ? (Size - count) : 0;
lock.unlock();
ReadyForPushCV.notify_all();
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notification strategy after EndPopMultiple should be consistent with EndPop. EndPop uses notify_one() (line 137), but EndPopMultiple uses notify_all() (line 146). This inconsistency could lead to unnecessary wake-ups. Consider using notify_one() here for consistency unless there's a specific reason multiple waiters need to be notified.

Suggested change
ReadyForPushCV.notify_all();
ReadyForPushCV.notify_one();

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +51
auto lcm = std::lcm(Ratio.x(), Ratio.y());
auto requiredEffectiveCapacity = lcm * Capacity;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculation of requiredEffectiveCapacity at line 51 can overflow if lcm * Capacity exceeds the maximum value of uint32_t. This could happen with large ratio values and capacity. Consider checking for potential overflow before the multiplication or using a larger integer type for the intermediate calculation.

Copilot uses AI. Check for mistakes.
ReadyForPushCV.notify_all();
}

RingBufferServeMode GetMode() const
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GetMode method should be marked as noexcept since it only returns a member variable and cannot throw exceptions. This is consistent with GetCapacity which is already marked noexcept.

Suggested change
RingBufferServeMode GetMode() const
RingBufferServeMode GetMode() const noexcept

Copilot uses AI. Check for mistakes.
{
nosPathCommand ringSizeChange{.Event = NOS_RING_SIZE_CHANGE, .RingSize = Capacity};
nosEngine.SendPathCommand(*GetPinId(NSN_Input), ringSizeChange);
CapacityUpdatedViaPathCommand = false;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CapacityUpdatedViaPathCommand flag is checked at line 291 to prevent sending a path command if it was already updated via path command, but then it's immediately set to false at line 295. This makes the check ineffective as the flag will always be false after this code executes. The assignment at line 295 should be removed, as the flag should remain true after being set and only be reset when the path command is actually processed.

Suggested change
CapacityUpdatedViaPathCommand = false;

Copilot uses AI. Check for mistakes.
auto requiredEffectiveCapacity = lcm * Capacity;
if (requiredEffectiveCapacity != EffectiveCapacity)
{
EffectiveCapacity = requiredEffectiveCapacity;
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After updating EffectiveCapacity at line 54, the Ring buffer should be reset to reflect the new capacity. However, there's no Ring.Reset call here. The reset only happens later when SendPathRestart is called at line 56, which will eventually trigger OnPathStart. Consider calling Ring.Reset(EffectiveCapacity) here to ensure the ring buffer capacity is consistent with EffectiveCapacity.

Suggested change
EffectiveCapacity = requiredEffectiveCapacity;
EffectiveCapacity = requiredEffectiveCapacity;
Ring.Reset(EffectiveCapacity);

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Reset should happen after path restart.

TypeInfo typeInfo(params->IncomingTypeName);
if (typeInfo->BaseType != NOS_BASE_TYPE_ARRAY)
{
strncpy(params->OutErrorMessage, "Connected pin type must be an array type!", 42);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strncpy call copies exactly 42 bytes, but the string "Connected pin type must be an array type!" is 43 characters long (including the null terminator). This will result in a non-null-terminated string in the error message buffer. Either increase the count to include the null terminator or use a safer alternative like snprintf.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Are you sure?


FrameRateConverterNode() : Ring(1, RingBufferServeMode::WaitUntilFull)
{
Ring.Reset(EffectiveCapacity);
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Ring.Reset call at line 34 does not specify the mode parameter, so it will keep the mode from the constructor. However, it's called before the EffectiveCapacity is calculated from the pin values. Since EffectiveCapacity is 1 at this point and will be recalculated later, this Reset call may be unnecessary or should be removed to avoid confusion.

Suggested change
Ring.Reset(EffectiveCapacity);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI commented Jan 7, 2026

@shamilatesoglu I've opened a new pull request, #47, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI commented Jan 7, 2026

@shamilatesoglu I've opened a new pull request, #48, to work on those changes. Once the pull request is ready, I'll request review from you.

@shamilatesoglu shamilatesoglu marked this pull request as draft January 7, 2026 14:19
@shamilatesoglu shamilatesoglu force-pushed the wip-frame-rate-converter branch from 3ca3cea to aedb559 Compare January 7, 2026 15:41
@shamilatesoglu shamilatesoglu force-pushed the wip-frame-rate-converter branch from 345dec4 to 73b13e5 Compare January 7, 2026 15:59
@shamilatesoglu shamilatesoglu marked this pull request as ready for review January 7, 2026 15:59
@shamilatesoglu shamilatesoglu merged commit c0cc667 into dev Jan 7, 2026
@caner-milko caner-milko deleted the wip-frame-rate-converter branch January 8, 2026 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants