6 votes

What programming/technical projects have you been working on?

This is a recurring post to discuss programming or other technical projects that we've been working on. Tell us about one of your recent projects, either at work or personal projects. What's interesting about it? Are you having trouble with anything?

5 comments

  1. [3]
    streblo
    (edited )
    Link
    Warning, premature optimization ahead. Not trying to write this efficiently, just having fun. :P So I started toying around with writing an Entity Component System (ECS) after reading through this...

    Warning, premature optimization ahead. Not trying to write this efficiently, just having fun. :P

    So I started toying around with writing an Entity Component System (ECS) after reading through this series. Briefly, the ideas is to break game entity data into components and store each component data in contiguous blocks of memory to minimize cache misses when you want to iterate over every component for associated entities.

    Right now I have a working sparse set to manage each block of components. Each set is composed of three containers: a large sparse vector to map entity IDs to component index, and two custom containers to pack components and entity IDs as tightly as possible, allowing for contiguous iteration over both.

    I wrote a custom container for the components because I dislike how std::vector value initializes anything you can index, so a resize on a large component vector could be quite costly. I understand why they do this, if you let people arbitrarily construct objects anywhere in allocated memory there is no way to iterate through and destruct all of them but as we are already tracking indexes in the sparse array we can just use the sparse set safely as a destructor or to manage resizing anyways. This lets us use placement new to constuct on demand anywhere in allocated space. In practice, we construct contiguously anyways but at least a resize only costs us an allocation and some moves now.

    (I suppose I could have pre-allocated everything on the stack but not sure if I want to limit the number of entities. I may end up writing a pool allocator in the future that pre-allocates, but it will include a method to get more blocks as required.)

    Anyways, my container uses std::aligned_storage as a method for allocating uninitialized blocks of memory I can placement new into:

        std::unique_ptr<typename std::aligned_storage<sizeof(T), alignof(T)>::type[]> data_;
    

    Which we can then emplace new objects into :

    template<typename... Args>
    void emplace (std::size_t index, Args&&... args)
    {
        if (index >= capacity_)
        {
            std::size_t new_size = (capacity_) ? capacity_ * growth_factor : 8u;
            resize(new_size);
        }
    		
    	
    	new (&data_.get()[index]) T(std::forward<Args>(args)...);
    	size_++;
    	
    }
    

    Erase objects:

    void erase(std::size_t index)
    {
        std::launder(reinterpret_cast<T*>(&data_.get()[index]))->~T();
    	size_--;
    }
    

    And access them by index:

    T& operator[](std::size_t index)
    {
        return *std::launder(reinterpret_cast<T*>(&data_.get()[index]));
    }
    
    const T& operator[](std::size_t index) const
    {
        return *std::launder(reinterpret_cast<const T*>(&data_.get()[index]));
    }
    

    Inserting and erasing from the sparse set is very simple:

    template<typename... Args>
    	void insert(const entity_id &val, Args&&... args)
    	{
    		if (!contains(val))
    		{
    			if (val >= capacity_)
    				grow(val); // resize sparse vec to val+1
    
    			sparse[val] = size_;
    			packed_entities.emplace(size_, val);
    			packed_components.emplace(size_, args...);
    			++size_;
    		}
    	}
    
    	void erase_entity(entity_id id)
    	{
    		if (contains(id))
    		{
    			sparse[packed_entities[size_ - 1]] = sparse[id];
    
    			packed_entities.erase(sparse[id]);
                packed_entities[sparse[id]] = packed_entities[size_ - 1];
    
                packed_components.erase(sparse[id]);
                packed_components[sparse[id]] = packed_components[size_ - 1];
    
    			--size_;
    		}
    	}
    

    I'm sure some of this is a bad idea, but given the constraints of a sparse set I can't see how the foot gun will bite me yet.

    5 votes
    1. [2]
      Moonchild
      (edited )
      Link Parent
      If you want to improve performance yet further, you're going to find 'sparse' is a problem; accessing a given entity directly by id requires multiple interdependent loads. One solution to this is...

      If you want to improve performance yet further, you're going to find 'sparse' is a problem; accessing a given entity directly by id requires multiple interdependent loads.

      One solution to this is to get rid of that mapping; instead, abstract over your data representation such that every time you move an entity, you can fix up all references to it to refer to the new ID. I don't know how practical this is in C++; I assume there are some introspection capabilities? (There's another good reason to have this capability: when debugging, removing an entity can fix up all references to it to an 'invalid id', catching certain classes of bugs without excessive space overhead.)

      Obviously, performing this fixup every time you remove an entity is not great for performance. So you can avoid actually moving anything when erasing an entity; just mark its slot as vacant. Then when inserting an entity, look for a vacant slot before adding onto the end. Most of the time, this will let you avoid needing to move anything or fix up any references. There is no time or space overhead, as you can use a freelist to track vacant indices. If you reach a certain fragmentation threshold—75%, say; but you can tune it—then you can move and fix things up.

      You can also avoid large copies when adding more entities than you have capacity for by referring to chunks indirectly. In principle, this affects locality, but not in a way that actually matters performance-wise. Traversing the entity list, each chunk will be quite large. And, having already gotten rid of the 'sparse' indirection, you can replace entity ids with pointers, so there will be no extra indirections when referring to a specific entity.

      Of course, this is a poor reinvention of compacting GC :)

      3 votes
      1. streblo
        (edited )
        Link Parent
        Thanks for the feedback! Yea, the tradeoff for the sparse set is that looking up an entity directly isn't great. However, the idea with the ECS is we minimize the amount we need to do that....

        Thanks for the feedback!

        If you want to improve performance yet further, you're going to find 'sparse' is a problem; accessing a given entity directly by id requires multiple interdependent loads.

        Yea, the tradeoff for the sparse set is that looking up an entity directly isn't great. However, the idea with the ECS is we minimize the amount we need to do that. Instead we have systems that operate on a component or multiple components and iterate through all of the components directly, performing some operations on the data. This is very simple when you just operate on one component but multiple components get a bit tricky. E.g. you have a movement system that operates on entities with both a potion and acceleration component -- you just iterate both arrays but how do you handle the ordering?

        What entt does (the ECS written by the guy with the blog series above) is provides a grouping system for components where you can keep grouped components ordered such that a movement system would simply iterate both with the same index up until some group delimter. (A group is essentially a pointer to the last element that both component arrays contain. When a new component is added that is already in the other grouped array you just swap it with the next available component after the pointer. Explained here.) The naive way to do it where you iterate the smallest component array and then directly look up the entitity in the other arrays would suffer from the indirection problem with the sparse array. I haven't decided what I'm going to do yet but I think offering some sort of grouping system is the way to go.

        I don't know how practical this is in C++; I assume there are some introspection capabilities?

        AFAIK yes it does but it I think it requires a deeper understanding of template metaprogramming than I currently have so for now I'll leave that to the SFINAE wizards. :)

        You've definitely given me some ideas to chew on here, thanks. It might be fun to revisit this ECS if I ever finish the engine and re-implement the interface using a pseudo-garbage collected approach and see how it compares.

        1 vote
  2. Surira
    Link
    I recently bought a macro pad on /r/mechmarket and have been teaching myself basic QMK and how to build and flash keyboard firmware. It's been going...OK...but I'm a total noob and all I've been...

    I recently bought a macro pad on /r/mechmarket and have been teaching myself basic QMK and how to build and flash keyboard firmware. It's been going...OK...but I'm a total noob and all I've been able to do is take a keymap someone else built and slightly tweak it. I have no idea how to program the OLED that's on my macropad, nor do I know how to do more than 3 layers using QMK tap dance or whatever it is you call that. As long as I don't brick the damn thing it should be a fun experiment at least

    2 votes
  3. spctrvl
    Link
    I'm converting my old Maker Select v2 (Duplicator i3 clone) to a 32-bit mainboard, the SKR 1.4 turbo, and adding an auto-leveling probe. The stock melzi board died on me last weekend and a drop in...

    I'm converting my old Maker Select v2 (Duplicator i3 clone) to a 32-bit mainboard, the SKR 1.4 turbo, and adding an auto-leveling probe. The stock melzi board died on me last weekend and a drop in replacement is much more expensive and much less functional than the upgrade. Using drv8825 stepper drivers for now since a 5 pack was just sitting on the shelf next to the board at micro center for $10, though I will eventually replace them with some TMC2226s (2209 successor) coming in a few months from china. The wiring is almost done, but I had to order a JST crimper and ends to wire in the extruder motor in a non-hacky way, and that's not here until Friday. Crossing my fingers that the firmware install will go smoothly, the web interface for customizing marlin seemed pretty straightforward, and I may even try it before wiring up the extruder.

    1 vote