Skip to content

refactor(scorch): Store scorch structs in a deque and various minor fixes - #3159

Open
stephanmeesters wants to merge 5 commits into
TheSuperHackers:mainfrom
stephanmeesters:refactor/scorches-deque
Open

refactor(scorch): Store scorch structs in a deque and various minor fixes#3159
stephanmeesters wants to merge 5 commits into
TheSuperHackers:mainfrom
stephanmeesters:refactor/scorches-deque

Conversation

@stephanmeesters

@stephanmeesters stephanmeesters commented Aug 16, 2026

Copy link
Copy Markdown

Merge by rebase

  • Fixes minor issue where a rejected (duplicated) scorch would still remove an old scorch unnecessarily
  • Correctness fix by releasing old scorch buffers before allocating a new one
  • Remove debug scorch marks, as they are not very useful
  • Refactor scorch deduplication and scorch buffer writes by extracting into their own functions
  • Refactor scorch objects by use of a std::deque instead of a fixed-size array

The std::deque is preferable because 1) it allows for a dynamic number of max scorches (if we were to increase in the future, and make quality level dependent); 2) performance.

Todo

  • Check that each commit compiles
  • Add pull ID to each commit

@stephanmeesters stephanmeesters added Gen Relates to Generals ZH Relates to Zero Hour Refactor Edits the code with insignificant behavior changes, is never user facing Rendering Is Rendering related labels Aug 16, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Scorch mark queue: dedupe before eviction and store scorches in std::deque

🐞 Bug fix ✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Fix scorch deduplication to avoid evicting an existing scorch on rejected duplicates.
• Prevent buffer leaks by releasing existing scorch buffers before reallocation.
• Refactor scorch storage to a deque and extract buffer-write logic for clarity.
Diagram

graph TD
A["addScorch()"] --> B{"Duplicate?"} -->|"yes"| C["Drop request"]
B -->|"no"| D["Scorch deque"] --> E["invalidateBuffers()"] --> F{"Needs recompute?"} -->|"yes"| G["writeScorchToBuffer()"] --> H[("DX8 VB/IB")]
F -->|"no"| I["Draw existing"]
subgraph Legend
  direction LR
  _mod["Module/Function"] ~~~ _dec{"Decision"} ~~~ _buf[("Buffer")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fixed-size ring buffer (circular array)
  • ➕ No per-element allocations; better cache locality than std::deque
  • ➕ Keeps stable memory footprint (important in render paths)
  • ➕ O(1) push/pop with explicit head/tail indices
  • ➖ More custom logic to maintain (wraparound, iteration order)
  • ➖ Harder to read than deque; higher bug risk if not well-tested
2. Keep array + head index (avoid shifting)
  • ➕ Minimal change from original approach
  • ➕ Avoids O(n) shifts while keeping fixed storage
  • ➖ Iteration becomes slightly more complex (index mapping)
  • ➖ Still custom bookkeeping compared to deque
3. std::vector + erase/front index
  • ➕ Contiguous storage (best cache) if managed carefully
  • ➕ Familiar container semantics
  • ➖ Front erases are O(n) unless using a head index (which reintroduces custom logic)
  • ➖ Potential reallocations unless reserved and carefully managed

Recommendation: Current deque-based approach is a reasonable readability/correctness tradeoff for a capped queue (MAX_SCORCH_MARKS). If profiling later shows scorch management is hot or allocation-heavy, consider a fixed-size ring buffer to remove deque block allocations and improve locality—while keeping the same dedupe-before-eviction behavior introduced here.

Files changed (2) +128 / -134

Bug fix (1) +120 / -131
W3DScorch.cppFix dedupe/eviction ordering, free buffers before reallocate, and extract buffer writer +120/-131

Fix dedupe/eviction ordering, free buffers before reallocate, and extract buffer writer

• Ensures duplicate scorches are rejected before evicting the oldest scorch, preventing unintended removals. Calls freeBuffers() at the start of allocateBuffers() to release old GPU resources before allocating new ones, and removes debug-only sample scorches. Refactors buffer rebuild to be gated by m_needBufferRecompute and extracts the per-scorch buffer population into writeScorchToBuffer() with explicit bounds failure signaling.

Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScorch.cpp

Refactor (1) +8 / -3
W3DScorch.hReplace fixed scorch array with std::deque and new helper declarations +8/-3

Replace fixed scorch array with std::deque and new helper declarations

• Introduces std::deque-backed scorch storage and replaces the previous (array + counters) model with a single container plus a dirty flag (m_needBufferRecompute). Adds private helpers for duplicate detection and writing a scorch into mapped vertex/index buffers.

Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScorch.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 16, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Signed diffuse parameter ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
writeScorchToBuffer takes diffuse as signed Int even though it is computed as UnsignedInt
and stored in VertexFormatXYZDUV1::diffuse (unsigned). This introduces an implementation-defined
unsigned→signed conversion and makes the API type-inaccurate (can also trigger signedness warnings).
Code

Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScorch.h[R80-81]

+	Bool writeScorchToBuffer(const TScorch& scorch, WorldHeightMap& map, Int diffuse,
+	                         VertexFormatXYZDUV1* curVb, UnsignedShort* curIb);
Evidence
The PR introduces a helper whose signature uses Int diffuse, but the value is produced as an
UnsignedInt color and ultimately assigned to an unsigned vertex diffuse field; this mismatch
forces an unsigned→signed conversion before storing back to unsigned.

Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScorch.h[78-82]
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScorch.cpp[154-165]
Core/Libraries/Source/WWVegas/WW3D2/dx8fvf.h[146-154]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeScorchToBuffer(...)` accepts `diffuse` as `Int`, but the caller produces an `UnsignedInt` color and the vertex format stores `diffuse` as unsigned. Keeping this parameter signed forces an implementation-defined conversion and misrepresents what the value is.
### Issue Context
- `updateScorches` computes `UnsignedInt diffuse = DX8Wrapper::Convert_Color_Clamp(...)` and passes it to `writeScorchToBuffer`.
- `VertexFormatXYZDUV1::diffuse` is declared as `unsigned`.
### Fix
- Change the `diffuse` parameter type in both declaration and definition from `Int` to `UnsignedInt` (preferred, since it matches the rest of the codebase) or to `unsigned`.
- Ensure the call site remains unchanged (it already has `UnsignedInt diffuse`).
### Fix Focus Areas
- Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScorch.h[78-82]
- Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DScorch.cpp[154-170]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DScorch.h Outdated
@stephanmeesters
stephanmeesters force-pushed the refactor/scorches-deque branch from 328f969 to 9414e54 Compare August 16, 2026 12:21
@stephanmeesters stephanmeesters changed the title refactor(scorch): Store scorch structs in a deque refactor(scorch): Store scorch structs in a deque and various minor fixes Aug 16, 2026
@Caball009

Copy link
Copy Markdown

The PR description doesn't include why std::deque would be preferable here. Can you include that?

@stephanmeesters

Copy link
Copy Markdown
Author

The PR description doesn't include why std::deque would be preferable here. Can you include that?

Done

@Caball009

Copy link
Copy Markdown

It's a bit of a tradeoff. It's clearly a win for the eviction of the oldest scorch mark, but a loss for memory access. An array + head & tail would be faster but make the code more complex so I wouldn't necessarily recommend it.

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixes and refactors look correct. But I am not sold on the std::deque. Maybe there is a better container type for it?

Int m_curNumScorchIndices; ///< number of indices used in m_indexScorch.
TScorch m_scorches[MAX_SCORCH_MARKS];
Int m_numScorches;
std::deque<TScorch> m_scorches;

@xezon xezon Aug 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A circular buffer would be the better data type for this I think.

I asked Chat Gippy to generate a Circular Buffer template class. Maybe you can try this kind of direction.

Chat

Code

Here’s a straightforward C++98-compatible circular buffer using a dynamically allocated array. It supports push/pop at both ends, indexing, and fixed capacity.

#include <cstddef>
#include <stdexcept>
#include <iterator>

template <typename T>
class CircularBuffer
{
public:
    class iterator;
    class const_iterator;

    explicit CircularBuffer(std::size_t capacity)
        : data_(capacity ? new T[capacity] : 0),
          capacity_(capacity),
          head_(0),
          size_(0)
    {
        if (capacity == 0)
            throw std::invalid_argument("capacity must be greater than zero");
    }

    ~CircularBuffer()
    {
        delete[] data_;
    }

private:
    CircularBuffer(const CircularBuffer&);
    CircularBuffer& operator=(const CircularBuffer&);

public:
    bool empty() const
    {
        return size_ == 0;
    }

    bool full() const
    {
        return size_ == capacity_;
    }

    std::size_t size() const
    {
        return size_;
    }

    std::size_t capacity() const
    {
        return capacity_;
    }

    void push_back(const T& value)
    {
        if (full())
            throw std::overflow_error("circular buffer is full");

        data_[index(size_)] = value;
        ++size_;
    }

    void pop_front()
    {
        if (empty())
            throw std::underflow_error("circular buffer is empty");

        head_ = next(head_);
        --size_;
    }

    T& front()
    {
        if (empty())
            throw std::underflow_error("circular buffer is empty");

        return data_[head_];
    }

    const T& front() const
    {
        if (empty())
            throw std::underflow_error("circular buffer is empty");

        return data_[head_];
    }

    T& back()
    {
        if (empty())
            throw std::underflow_error("circular buffer is empty");

        return data_[index(size_ - 1)];
    }

    const T& back() const
    {
        if (empty())
            throw std::underflow_error("circular buffer is empty");

        return data_[index(size_ - 1)];
    }

    T& operator[](std::size_t i)
    {
        return data_[index(i)];
    }

    const T& operator[](std::size_t i) const
    {
        return data_[index(i)];
    }

    void clear()
    {
        head_ = 0;
        size_ = 0;
    }

    iterator begin()
    {
        return iterator(this, 0);
    }

    iterator end()
    {
        return iterator(this, size_);
    }

    const_iterator begin() const
    {
        return const_iterator(this, 0);
    }

    const_iterator end() const
    {
        return const_iterator(this, size_);
    }

    const_iterator cbegin() const
    {
        return const_iterator(this, 0);
    }

    const_iterator cend() const
    {
        return const_iterator(this, size_);
    }

private:
    std::size_t next(std::size_t i) const
    {
        return (i + 1) % capacity_;
    }

    std::size_t index(std::size_t i) const
    {
        return (head_ + i) % capacity_;
    }

    T* data_;
    std::size_t capacity_;
    std::size_t head_;
    std::size_t size_;

public:

    class iterator : public std::iterator<std::random_access_iterator_tag, T>
    {
    public:
        iterator()
            : buffer_(0), pos_(0)
        {
        }

        iterator(CircularBuffer* buffer, std::size_t pos)
            : buffer_(buffer), pos_(pos)
        {
        }

        T& operator*() const
        {
            return (*buffer_)[pos_];
        }

        T* operator->() const
        {
            return &(*buffer_)[pos_];
        }

        iterator& operator++()
        {
            ++pos_;
            return *this;
        }

        iterator operator++(int)
        {
            iterator tmp(*this);
            ++(*this);
            return tmp;
        }

        iterator& operator--()
        {
            --pos_;
            return *this;
        }

        iterator operator--(int)
        {
            iterator tmp(*this);
            --(*this);
            return tmp;
        }

        iterator operator+(std::ptrdiff_t n) const
        {
            return iterator(buffer_, pos_ + n);
        }

        iterator operator-(std::ptrdiff_t n) const
        {
            return iterator(buffer_, pos_ - n);
        }

        std::ptrdiff_t operator-(const iterator& other) const
        {
            return static_cast<std::ptrdiff_t>(pos_) -
                   static_cast<std::ptrdiff_t>(other.pos_);
        }

        iterator& operator+=(std::ptrdiff_t n)
        {
            pos_ += n;
            return *this;
        }

        iterator& operator-=(std::ptrdiff_t n)
        {
            pos_ -= n;
            return *this;
        }

        T& operator[](std::ptrdiff_t n) const
        {
            return (*buffer_)[pos_ + n];
        }

        bool operator==(const iterator& other) const
        {
            return buffer_ == other.buffer_ && pos_ == other.pos_;
        }

        bool operator!=(const iterator& other) const
        {
            return !(*this == other);
        }

        bool operator<(const iterator& other) const
        {
            return pos_ < other.pos_;
        }

        bool operator>(const iterator& other) const
        {
            return other < *this;
        }

        bool operator<=(const iterator& other) const
        {
            return !(other < *this);
        }

        bool operator>=(const iterator& other) const
        {
            return !(*this < other);
        }

    private:
        CircularBuffer* buffer_;
        std::size_t pos_;
    };

    class const_iterator
        : public std::iterator<std::random_access_iterator_tag, const T>
    {
    public:
        const_iterator()
            : buffer_(0), pos_(0)
        {
        }

        const_iterator(const CircularBuffer* buffer, std::size_t pos)
            : buffer_(buffer), pos_(pos)
        {
        }

        const T& operator*() const
        {
            return (*buffer_)[pos_];
        }

        const T* operator->() const
        {
            return &(*buffer_)[pos_];
        }

        const_iterator& operator++()
        {
            ++pos_;
            return *this;
        }

        const_iterator operator++(int)
        {
            const_iterator tmp(*this);
            ++(*this);
            return tmp;
        }

        const_iterator& operator--()
        {
            --pos_;
            return *this;
        }

        const_iterator operator--(int)
        {
            const_iterator tmp(*this);
            --(*this);
            return tmp;
        }

        const_iterator operator+(std::ptrdiff_t n) const
        {
            return const_iterator(buffer_, pos_ + n);
        }

        const_iterator operator-(std::ptrdiff_t n) const
        {
            return const_iterator(buffer_, pos_ - n);
        }

        std::ptrdiff_t operator-(const const_iterator& other) const
        {
            return static_cast<std::ptrdiff_t>(pos_) -
                   static_cast<std::ptrdiff_t>(other.pos_);
        }

        const_iterator& operator+=(std::ptrdiff_t n)
        {
            pos_ += n;
            return *this;
        }

        const_iterator& operator-=(std::ptrdiff_t n)
        {
            pos_ -= n;
            return *this;
        }

        const T& operator[](std::ptrdiff_t n) const
        {
            return (*buffer_)[pos_ + n];
        }

        bool operator==(const const_iterator& other) const
        {
            return buffer_ == other.buffer_ && pos_ == other.pos_;
        }

        bool operator!=(const const_iterator& other) const
        {
            return !(*this == other);
        }

        bool operator<(const const_iterator& other) const
        {
            return pos_ < other.pos_;
        }

        bool operator>(const const_iterator& other) const
        {
            return other < *this;
        }

        bool operator<=(const const_iterator& other) const
        {
            return !(other < *this);
        }

        bool operator>=(const const_iterator& other) const
        {
            return !(*this < other);
        }

    private:
        const CircularBuffer* buffer_;
        std::size_t pos_;
    };
};

Example

#include <iostream>

int main()
{
    CircularBuffer<int> buffer(3);

    buffer.push_back(10);
    buffer.push_back(20);
    buffer.push_back(30);

    std::cout << buffer.front() << std::endl; // 10
    std::cout << buffer.back()  << std::endl; // 30

    buffer.pop_front();
    buffer.push_back(40);

    for (std::size_t i = 0; i < buffer.size(); ++i)
        std::cout << buffer[i] << " ";

    // Output: 20 30 40
    return 0;
}

The key idea is that head_ points to the logical first element, while size_ tracks how many elements are currently stored. Physical positions wrap using % capacity_, so insertion/removal from the ends is O(1) and indexing is also O(1).

Iterator usage is STL-like:

CircularBuffer<int> buffer(5);

buffer.push_back(10);
buffer.push_back(20);
buffer.push_back(30);

CircularBuffer<int>::iterator it;

for (it = buffer.begin(); it != buffer.end(); ++it)
    std::cout << *it << " ";

// 10 20 30

Because the iterator uses a logical position rather than a raw pointer, iteration works correctly even when the buffer wraps around physically:

Physical storage:
[40][50][30][  ][  ]
        ^
       head

Logical iteration:
30 -> 40 -> 50

One C++98 caveat: std::iterator was deprecated much later and removed in C++20, but it is valid C++98. If you want the class to be compatible with both C++98 and modern C++, I would avoid inheriting from std::iterator and define the iterator typedefs directly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe just put a 'todo' to refactor this when we drop C++98 compatibility and keep it simple for now.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Circular Buffer class does not exist in c++20 either. So needs impl anyway.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Some things can be simplified in C++20.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Would agree with Caball, then we could source a properly made version of the circular buffer like this one

TScorch scorch;
scorch.location = location;
scorch.radius = radius;
scorch.scorchType = type < 0 || (Int)type >= SCORCH_MARKS_IN_TEXTURE ? SCORCH_1 : type;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This test is new. Maybe make it an assert instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gen Relates to Generals Refactor Edits the code with insignificant behavior changes, is never user facing Rendering Is Rendering related ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants