Performance Impact Question

Just a quick question what the best method for picking items off belts is, when completing custom recipes. I'd guess using the native manufacture implementation would be the best. Looking at an ancient copy of refined power they do something like this
c++
void ARPMPHeaterBuilding::CollectItems(float dt)
{
if (InputFuelConveyor)
{
for (auto AllowedItem : mAllowedFuelItems)
{
if (CanStoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 1))
{
FInventoryItem outItem;
float offset = 0.0f;
bool pulledItem = InputFuelConveyor->Factory_GrabOutput(outItem, offset, AllowedItem);
if (pulledItem)
{
StoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 1);
}
}
}
}

if (InputFuelPipe)
{
for (auto AllowedItem : mAllowedFuelItems)
{
if (CanStoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 100))
{
FInventoryStack outStack;
bool pulledItem = InputFuelPipe->Factory_PullPipeInput(dt, outStack, AllowedItem, 100);
if (pulledItem)
{
StoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 100);
}
}
}
}
}
c++
void ARPMPHeaterBuilding::CollectItems(float dt)
{
if (InputFuelConveyor)
{
for (auto AllowedItem : mAllowedFuelItems)
{
if (CanStoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 1))
{
FInventoryItem outItem;
float offset = 0.0f;
bool pulledItem = InputFuelConveyor->Factory_GrabOutput(outItem, offset, AllowedItem);
if (pulledItem)
{
StoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 1);
}
}
}
}

if (InputFuelPipe)
{
for (auto AllowedItem : mAllowedFuelItems)
{
if (CanStoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 100))
{
FInventoryStack outStack;
bool pulledItem = InputFuelPipe->Factory_PullPipeInput(dt, outStack, AllowedItem, 100);
if (pulledItem)
{
StoreItemInInventory(GetMPInventory(), mInputInvIndex, AllowedItem, 100);
}
}
}
}
}
Which I think can be optimized a bit... but if every single building is running O(n) loops... I figure this will get pretty slow pretty fast. I could improve on this by not looping if we already have an item in the building, so we don't need to check if the item on the belt is in our allow list, and instead just need to validate if it is what is loaded. Similarly this function can be skipped entirely if our inventory is full. Simple stuff like that... I'm sure a more recent version of refined power has much more sophisticated code...
27 Replies
MrWolf
MrWolfOPā€¢4w ago
Looking at nogs mods we're doing something like this
c++
void ANogsBuildableResearcher::Factory_Tick(float dt)
{
Super::Factory_Tick(dt);
if (HasAuthority() && IsValid(this) && Pipe && GetStorageInventory()) {
if (Factory_HasPower()) {
if (Pipe->IsConnected()) {
if (GetStorageInventory() && GetStorageInventory()->IsSomethingOnIndex(0)) {
FInventoryStack CurrentItem; GetStorageInventory()->GetStackFromIndex(0, CurrentItem);
if (CurrentItem.Item.GetItemClass()) {
FInventoryStack Stack;
Pipe->Factory_PullPipeInput(dt, Stack, CurrentItem.Item.GetItemClass(), FMath::Clamp((5000.f * mFluidStackSizeMultiplier) - CurrentItem.NumItems, 0.f, 300.f * dt));
if (Stack.HasItems()) {
GetStorageInventory()->AddStackToIndex(0, Stack);
}
}
}
else {
FInventoryStack Stack;
Pipe->Factory_PullPipeInput(dt, Stack, nullptr, FMath::Clamp(5000.f * mFluidStackSizeMultiplier, 0.f, 300.f * dt));
if (Stack.HasItems()) {
GetStorageInventory()->AddStackToIndex(0, Stack);
}
}
}
}
}
}
c++
void ANogsBuildableResearcher::Factory_Tick(float dt)
{
Super::Factory_Tick(dt);
if (HasAuthority() && IsValid(this) && Pipe && GetStorageInventory()) {
if (Factory_HasPower()) {
if (Pipe->IsConnected()) {
if (GetStorageInventory() && GetStorageInventory()->IsSomethingOnIndex(0)) {
FInventoryStack CurrentItem; GetStorageInventory()->GetStackFromIndex(0, CurrentItem);
if (CurrentItem.Item.GetItemClass()) {
FInventoryStack Stack;
Pipe->Factory_PullPipeInput(dt, Stack, CurrentItem.Item.GetItemClass(), FMath::Clamp((5000.f * mFluidStackSizeMultiplier) - CurrentItem.NumItems, 0.f, 300.f * dt));
if (Stack.HasItems()) {
GetStorageInventory()->AddStackToIndex(0, Stack);
}
}
}
else {
FInventoryStack Stack;
Pipe->Factory_PullPipeInput(dt, Stack, nullptr, FMath::Clamp(5000.f * mFluidStackSizeMultiplier, 0.f, 300.f * dt));
if (Stack.HasItems()) {
GetStorageInventory()->AddStackToIndex(0, Stack);
}
}
}
}
}
}
The nested if's here could use some love... but at lest this runs O(1). Still tho, that is a lot of logic on every single tic... Does anyone have an answer? I understand I may be way out in uncharted water... but I figure that I should at least poll the community to figure out what has been done before me. Maybe there is a way to hyjack the manufacturer code? I did try to hyjack that code... by injecting item slots into the input and output inventor by overloading the setRecipe function... I hoped to be able to add custom addtional items a manufacturer can consume. In my case fuel and byproducts. My next thought is to try and implement my manufacturers as two manufacturers. Each with a settable recipe. I think this could be accomplished by making a buildable, that contains two child actors of type Buildable Manufacturer. The trick would be in the widget and how the components are assigned or exposed to the child actors. But I havent made it too far down this path of research becasue I wanted to see what y'all have come up with first. Maybe there is an easier path forward.
Rex
Rexā€¢4w ago
It doesn't look too bad, three of the if's are just checking if things are valid Though one of the GetStorageInventory() calls is redundant (already checked earlier)
MrWolf
MrWolfOPā€¢4w ago
I dont think GetStorageInventory has any performance impact because it should be FORCEDINLINE replaced with a pointer
Rex
Rexā€¢4w ago
I'm just noting that the return value is checked twice
if (HasAuthority() && IsValid(this) && Pipe && GetStorageInventory()) { // <--- Here
if (Factory_HasPower()) {
if (Pipe->IsConnected()) {
if (GetStorageInventory() && GetStorageInventory()->IsSomethingOnIndex(0)) { // <--- Here
if (HasAuthority() && IsValid(this) && Pipe && GetStorageInventory()) { // <--- Here
if (Factory_HasPower()) {
if (Pipe->IsConnected()) {
if (GetStorageInventory() && GetStorageInventory()->IsSomethingOnIndex(0)) { // <--- Here
MrWolf
MrWolfOPā€¢4w ago
How did you make that colored?? Oh neat... I totally missed that, thanks. Problem with this implementation is that it doesn't validate inputs just kinda sucks stuff up' I guess that'll work fine tho... because we can validate once it's in the inventory its a depatrure from Satisfactorys wa of doing things... but... it might be more efficent?
Rex
Rexā€¢4w ago
For items there's some peek function
MrWolf
MrWolfOPā€¢4w ago
Do we know the logic tic per second? In starcraft the logic tics are 23.81 updates per second, while graphics can draw at whatever they want.
Rex
Rexā€¢4w ago
ĀÆ\_(惄)_/ĀÆ I'd suggest doing things in the same way as the game does them, that is to have your buildables pull in resources from connections in Factory_Tick
MrWolf
MrWolfOPā€¢4w ago
I think that is essentially required, as factory tics operate in parallel, and dont sleep... unlike tics. Im just thinking, if I have 4x 1200 item per minute belts... thats 20 items per second per belt. If we are pulling in one, and only one... at a time, because we can create sushi belts... Then I need to be checking inputs at least 20 times per second... per building. How I do that if you have lets say 100 buildings with four inputs each... 8000 checks per second. I'd think... this would eventually fall apart.
Rex
Rexā€¢4w ago
They run in parallel but still in lockstep with the game thread VS has a built-in profiler, you can use it to see if your code is slow Sometimes it's not your code per se, it's the functions you call from your code.
MrWolf
MrWolfOPā€¢4w ago
Google seems to think we are operating at 60 Factory Ticks per second. Which would imply, if my maximum input is 20 items per second... that I am checking 3 times as often as I need to. Alright... with that knowledge in mind... I'll write up some basic functionality, and if we hit performance concern, I should be able to speed it up 3x at least... by dividing the machines up into three groups. Thanks Rex! Will do my best
Rex
Rexā€¢4w ago
I'm confused, where would Google get those numbers from
MrWolf
MrWolfOPā€¢4w ago
The phrase "Google seems to think" implies... I googled it... and clicked a bunch of links... and read a bunch... until a number fell out. šŸ˜‹ Sorry... bad habbit. Formed because Dr. Google is a common phrase I like to use when communicating with medical professionals because it implies that I understand that google research isn't as accurate as a medical opinion while also articulating that I did some research. Instead of something like.... but I read on this website or that website or... doesnt that mean this because that, etc. suggesting Im second guessing their professional option... Which some don't appreciate... Im just articulating confusion and curiosity and a willingness to learn and understand.
Rex
Rexā€¢4w ago
I didn't read the paragraph above. My point is, how reliable is that number? Do you need to account for it when making a machine?
MrWolf
MrWolfOPā€¢4w ago
Not very reliable, intend on testing. In theory, as we have a delta time variable in FactoryTick. as soon as I drop that somewhere readable we should have an answer what our upper bound is. In theory... this function should be called n times, where n is the number of times that tick should have fit inside of delta time. Ideally this will always be 1. However as the computational load grows, and tics are missed, additional ticks should be issued inside of one tick block to keep the logic constant. But... that is all academic until I test it. I just feel better starting down this path knowing that there are optimizations I can do, on my side... to help alleviate performance issues. If I start down this path with no options available... I might be better off trying to figure out how to leverage CSS implementations, as anything I do would just be worse. @Rex [they/them] Based on our pass by reference conversation, then it would appear RRD's implementation of CollectItems isn't great because we are using CanStoreItemInInventory, which looks like this
c++
bool ARPMPBuilding::CanStoreItemInInventory(UFGInventoryComponent* inventory, int InvIndex,
TSubclassOf<UFGItemDescriptor> itemClass, int amount)
{
if (!inventory || inventory->GetSizeLinear() == 0) return false;

FInventoryStack out_stack;
inventory->GetStackFromIndex(InvIndex, out_stack);
if (out_stack.HasItems())
{
if (itemClass != out_stack.Item.ItemClass)
{
return false;
}

int stackSize = UFGItemDescriptor::GetStackSize(out_stack.Item.ItemClass);

if (out_stack.NumItems >= stackSize || (out_stack.NumItems + amount) > stackSize)
{
return false;
}
}

return true;
}
c++
bool ARPMPBuilding::CanStoreItemInInventory(UFGInventoryComponent* inventory, int InvIndex,
TSubclassOf<UFGItemDescriptor> itemClass, int amount)
{
if (!inventory || inventory->GetSizeLinear() == 0) return false;

FInventoryStack out_stack;
inventory->GetStackFromIndex(InvIndex, out_stack);
if (out_stack.HasItems())
{
if (itemClass != out_stack.Item.ItemClass)
{
return false;
}

int stackSize = UFGItemDescriptor::GetStackSize(out_stack.Item.ItemClass);

if (out_stack.NumItems >= stackSize || (out_stack.NumItems + amount) > stackSize)
{
return false;
}
}

return true;
}
Which would imply on every Factory Tick for every single input for every single building, we are making a copy of a stack. I'm thinking the less memory copying the better. Is my analysis about right? Which implies my implementation isn't going to work either... as I'm copying one or more FInventoryItems per input per factory tick on every building. I haven't found a way to just grab a pointer to an inventory so I can just look at it without the copy.
c++
void AWolfLibBuildableFactory::GrabBelt( WolfLibConnectionComponent* Component )
{
// Sanity Check Inventory
if (!Component->Inventory || Component->Inventory->GetSizeLinear() == 0 ) return;
UFGFactoryConnectionComponent* BeltComponent = Cast<UFGFactoryConnectionComponent>(Component->Connection);
// Sanity Check Belt Component
if (!BeltComponent->IsConnected()) return;
TArray< FInventoryItem > OutItems;
if (!BeltComponent->Factory_PeekOutput(OutItems)) return;
float Offset = 0.0f;
FInventoryItem GrabbedItem;
TSubclassOf<UFGItemDescriptor> ItemClass;
/* Typically of length 1 */
for (FInventoryItem Item : OutItems)
{
ItemClass = Item.GetItemClass();
if (!Component->AllowAny)
{
if (Component->AllowedItems.Find(ItemClass) == INDEX_NONE) continue;
}
if (Component->Inventory->HasEnoughSpaceForItem(Item)) {
if (BeltComponent->Factory_GrabOutput(GrabbedItem, Offset, ItemClass))
{
Component->Inventory->AddItem(GrabbedItem);
}
}
}
}
c++
void AWolfLibBuildableFactory::GrabBelt( WolfLibConnectionComponent* Component )
{
// Sanity Check Inventory
if (!Component->Inventory || Component->Inventory->GetSizeLinear() == 0 ) return;
UFGFactoryConnectionComponent* BeltComponent = Cast<UFGFactoryConnectionComponent>(Component->Connection);
// Sanity Check Belt Component
if (!BeltComponent->IsConnected()) return;
TArray< FInventoryItem > OutItems;
if (!BeltComponent->Factory_PeekOutput(OutItems)) return;
float Offset = 0.0f;
FInventoryItem GrabbedItem;
TSubclassOf<UFGItemDescriptor> ItemClass;
/* Typically of length 1 */
for (FInventoryItem Item : OutItems)
{
ItemClass = Item.GetItemClass();
if (!Component->AllowAny)
{
if (Component->AllowedItems.Find(ItemClass) == INDEX_NONE) continue;
}
if (Component->Inventory->HasEnoughSpaceForItem(Item)) {
if (BeltComponent->Factory_GrabOutput(GrabbedItem, Offset, ItemClass))
{
Component->Inventory->AddItem(GrabbedItem);
}
}
}
}
My implementation might actually be worse, because I am copying more then one FInventoryItem. A minimum of two, one from the peek, and one from the grab. Plus up to N copies where N is the number of OutItems our connection can see. But on the other hand, I am conducting fewer GrabOutput calls. Im checking if the items we can grab are on the allowed list... where as the example code tries to grab each of the allowed items from the input. But I guess that means that theirs will be faster then mine in any case where there is only one allowed input, because I peek first so I call twice in all cases. Theres gotta be a better way to do this
Rex
Rexā€¢4w ago
Where is that implementation from?
Rex
Rexā€¢4w ago
Okay, that's ancient code
Rex
Rexā€¢4w ago
No description
MrWolf
MrWolfOPā€¢4w ago
I said that a long time ago. Either way, its one of the two best examples I've seen. Nogs, shown above... also handles this by copying stacks. Think I may have found a path forward tho... give me a bit
c++
void AWolfLibBuildableFactory::GrabBelt(WolfLibConnectionComponent* Component)
{
if (!Component->Inventory || Component->Inventory->GetSizeLinear() == 0) return;
UFGFactoryConnectionComponent* BeltComponent = Cast<UFGFactoryConnectionComponent>(Component->Connection);
if (!BeltComponent->IsConnected()) return;
UFGFactoryConnectionComponent* OtherComponent = BeltComponent->GetConnection();
UFGInventoryComponent* OtherInventory = OtherComponent->GetInventory();
if (!OtherInventory || OtherInventory->GetSizeLinear() == 0) return;
if (OtherInventory->IsEmpty()) return;
uint32 OtherInventoryIndex = OtherComponent->GetInventoryAccessIndex();
TSubclassOf<UFGItemDescriptor> ItemClass;

if ( OtherInventoryIndex != -1 )
{ // Output Connection is tied to an inventory slot
ItemClass = OtherInventory->GetAllowedItemOnIndex( OtherInventoryIndex );
if (ItemClass != nullptr)
{ // Output Only Allows One Specific Item
if (Component->AllowAny || Component->AllowedItems.Find(ItemClass) != INDEX_NONE)
{ // Input Allows Any OR Output's One Item Is Allowed By Input

}
}
else
{ // Output Can Hold Any One Item

}
}
else
{ // Output Connection can pull from any inventory slot

}
c++
void AWolfLibBuildableFactory::GrabBelt(WolfLibConnectionComponent* Component)
{
if (!Component->Inventory || Component->Inventory->GetSizeLinear() == 0) return;
UFGFactoryConnectionComponent* BeltComponent = Cast<UFGFactoryConnectionComponent>(Component->Connection);
if (!BeltComponent->IsConnected()) return;
UFGFactoryConnectionComponent* OtherComponent = BeltComponent->GetConnection();
UFGInventoryComponent* OtherInventory = OtherComponent->GetInventory();
if (!OtherInventory || OtherInventory->GetSizeLinear() == 0) return;
if (OtherInventory->IsEmpty()) return;
uint32 OtherInventoryIndex = OtherComponent->GetInventoryAccessIndex();
TSubclassOf<UFGItemDescriptor> ItemClass;

if ( OtherInventoryIndex != -1 )
{ // Output Connection is tied to an inventory slot
ItemClass = OtherInventory->GetAllowedItemOnIndex( OtherInventoryIndex );
if (ItemClass != nullptr)
{ // Output Only Allows One Specific Item
if (Component->AllowAny || Component->AllowedItems.Find(ItemClass) != INDEX_NONE)
{ // Input Allows Any OR Output's One Item Is Allowed By Input

}
}
else
{ // Output Can Hold Any One Item

}
}
else
{ // Output Connection can pull from any inventory slot

}
I can avoid coping memory entirely... if I implement my own Component->Inventory->HasEnoughSpaceForItem() I have... no clue if this is a better approach tho
Rex
Rexā€¢4w ago
Does this work? I'm not sure, but it seems you're getting the inventory of whatever is on the other end of the belt?
MrWolf
MrWolfOPā€¢4w ago
I think that connection would be the belt. Every belt has an input/output UFGFactoryConnectionComponent as I understand it. Similarly our Factory has a UFGFactoryConnectionComponent. So when we call Factory_GrabOutput on out Factories UFGFactoryComponent, I think that it is grabbing from the output of the connected UFGFactoryConnectionComponent (The Belt). I don't think you can get the Input Component from the Belt, just the Output. Unfortunately. Otherwise, we could navigate belt networks like linked lists šŸ˜‹ Which in my opinion would be very VERY cool. Oh lol... you totally can... because you can get the outer buildable, you can walk from your connection component up into the buildable, and then down into an input connection component and into another output connection, up into the buildable... Wow... pretty sure once you've mapped an entire network you could do some crazy optimization somehow. Or add some crazy features.
Rex
Rexā€¢4w ago
It's not a good idea because of threading
MrWolf
MrWolfOPā€¢4w ago
Sorry whats not a good idea? Oh to map the whole network and act on it? Yea Im sure there would be some hickups... but Im trying to create the best grab function I can right now
c++
void AWolfLibBuildableFactory::GrabBelt(WolfLibConnectionComponent* InputComponent)
{
if (!InputComponent->Inventory || InputComponent->Inventory->GetSizeLinear() == 0) return;
UFGFactoryConnectionComponent* InputBeltComponent = Cast<UFGFactoryConnectionComponent>(InputComponent->Connection);
if (!InputBeltComponent->IsConnected()) return;
UFGFactoryConnectionComponent* OutputBeltComponent = InputBeltComponent->GetConnection();
UFGInventoryComponent* OutputInventory = OutputBeltComponent->GetInventory();
if (!OutputInventory || OutputInventory->GetSizeLinear() == 0) return;
if (OutputInventory->IsEmpty()) return;
uint32 OutputInventoryIndex = OutputBeltComponent->GetInventoryAccessIndex();
TSubclassOf<UFGItemDescriptor> ItemClass;
uint32 OutputIndex = 0;
uint32 EndIndex = OutputInventory->GetSizeLinear();
if (OutputInventoryIndex != -1){
OutputIndex = OutputInventoryIndex;
EndIndex = OutputInventoryIndex;
}
do {
if (OutputInventory->IsIndexEmpty(OutputIndex)){
OutputIndex++;
continue;
}
ItemClass = OutputInventory->GetAllowedItemOnIndex(OutputIndex);
if (ItemClass != nullptr) {
if (InputComponent->AllowAny || InputComponent->AllowedItems.Find(ItemClass) != INDEX_NONE) {
// We Can Grab This ItemClass
}
} else {
if (InputComponent->AllowAny) {
// !!! We May Be Doomed To Peek In This Case !!! //
} else {
for (TSubclassOf<UFGItemDescriptor>ItemClass : InputComponent->AllowedItems) {
if (OutputInventory->HasItems(ItemClass, 1)) { // It Exists!
// We Can Grab This ItemClass
}
}
}
}
OutputIndex++;
} while (OutputIndex < EndIndex);
}
c++
void AWolfLibBuildableFactory::GrabBelt(WolfLibConnectionComponent* InputComponent)
{
if (!InputComponent->Inventory || InputComponent->Inventory->GetSizeLinear() == 0) return;
UFGFactoryConnectionComponent* InputBeltComponent = Cast<UFGFactoryConnectionComponent>(InputComponent->Connection);
if (!InputBeltComponent->IsConnected()) return;
UFGFactoryConnectionComponent* OutputBeltComponent = InputBeltComponent->GetConnection();
UFGInventoryComponent* OutputInventory = OutputBeltComponent->GetInventory();
if (!OutputInventory || OutputInventory->GetSizeLinear() == 0) return;
if (OutputInventory->IsEmpty()) return;
uint32 OutputInventoryIndex = OutputBeltComponent->GetInventoryAccessIndex();
TSubclassOf<UFGItemDescriptor> ItemClass;
uint32 OutputIndex = 0;
uint32 EndIndex = OutputInventory->GetSizeLinear();
if (OutputInventoryIndex != -1){
OutputIndex = OutputInventoryIndex;
EndIndex = OutputInventoryIndex;
}
do {
if (OutputInventory->IsIndexEmpty(OutputIndex)){
OutputIndex++;
continue;
}
ItemClass = OutputInventory->GetAllowedItemOnIndex(OutputIndex);
if (ItemClass != nullptr) {
if (InputComponent->AllowAny || InputComponent->AllowedItems.Find(ItemClass) != INDEX_NONE) {
// We Can Grab This ItemClass
}
} else {
if (InputComponent->AllowAny) {
// !!! We May Be Doomed To Peek In This Case !!! //
} else {
for (TSubclassOf<UFGItemDescriptor>ItemClass : InputComponent->AllowedItems) {
if (OutputInventory->HasItems(ItemClass, 1)) { // It Exists!
// We Can Grab This ItemClass
}
}
}
}
OutputIndex++;
} while (OutputIndex < EndIndex);
}
Any thoughts on what to do if both the input and the output are unfiltered inventories? Am I stuck peeking in that case?
Rex
Rexā€¢4w ago
From a factory buildable's Factory_Tick, accessing the inventory of another buildable (that is not a conveyor) is not thread-safe
MrWolf
MrWolfOPā€¢4w ago
And here is where I run up against the limits of my understanding. Having my MS in Computer Engineering and the bulk of my work much lower in the stack... The highest level thing I've ever written was pieces of OpenShift to implement OpenShift on PowerVS and PowerVC šŸ˜‹ I totally understand that the Factory Ticks are executed in parallel. Hence the caution. But in theory... as Satisfactory owns all of the memory for all of its threads... couldn't I "safeishly" Read Thread B's memory From Thread A, while Thread B acts on it? Or does that just count as an access violation and seg fault? If it doesn't seg fault... if I'm reading memory that is being written, I'd guess we'd just get garbage back. But as I am just traversing the linked list... We "shouldn't" be modifying the connection data often. Similarly... I think, if we did get garbage back... that it might be possible to validate if we had received valid data or not. Wonder why the conveyor is exempt from this issue. I'd have thought it would follow the same rules as everything else.
Beef
Beefā€¢4w ago
Software Engineering Stack Exchange
Is a 1:* write:read thread system safe?
Theoretically, thread-safe code should fix race conditions. Race conditions, as I understand it, occur because two threads attempt to write to the same location at the same time. However, what abo...
Want results from more Discord servers?
Add your server