mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-10-23 13:33:03 +00:00
Prepare 0.1.48-alpha: optimize SD transports and repair map lifecycle
This commit is contained in:
1 parent
d7a2f35328
commit
8eeccc7b6b
67 files changed
+3880
-419
No files matched your search
@@ -42,6 +42,35 @@ struct LoadTileCommand
|
||||
MapTileRef tile{};
|
||||
};
|
||||
|
||||
struct TileRequestHandle
|
||||
{
|
||||
uint32_t generation = 0;
|
||||
uint32_t command_id = 0;
|
||||
explicit operator bool() const { return generation != 0 && command_id != 0; }
|
||||
bool matches(uint32_t event_generation, uint32_t event_id) const
|
||||
{
|
||||
return *this && generation == event_generation && command_id == event_id;
|
||||
}
|
||||
};
|
||||
|
||||
enum class TileSubmitStatus : uint8_t
|
||||
{
|
||||
Accepted,
|
||||
AlreadyPending,
|
||||
Backpressured,
|
||||
Unavailable
|
||||
};
|
||||
|
||||
struct TileSubmitResult
|
||||
{
|
||||
TileSubmitStatus status = TileSubmitStatus::Unavailable;
|
||||
TileRequestHandle handle{};
|
||||
explicit operator bool() const
|
||||
{
|
||||
return status == TileSubmitStatus::Accepted || status == TileSubmitStatus::AlreadyPending;
|
||||
}
|
||||
};
|
||||
|
||||
using MapTileEventKind = MapTileAsyncEventKind;
|
||||
|
||||
class MapTileEvent
|
||||
@@ -55,6 +84,11 @@ class MapTileEvent
|
||||
MapTilePayload payload{};
|
||||
std::size_t payload_size = 0;
|
||||
int32_t error = 0;
|
||||
MapTileReadTiming read_timing{};
|
||||
uint32_t command_wait_ms = 0;
|
||||
uint32_t worker_started_ms = 0;
|
||||
uint32_t published_ms = 0;
|
||||
bool timing_available = false;
|
||||
};
|
||||
|
||||
using MapTileAsyncEvent = MapTileEvent;
|
||||
@@ -93,16 +127,36 @@ class IMapTileCommandSink
|
||||
public:
|
||||
virtual ~IMapTileCommandSink() = default;
|
||||
|
||||
virtual bool enqueue(const LoadTileCommand& command) = 0;
|
||||
virtual TileSubmitResult enqueue(const LoadTileCommand& command) = 0;
|
||||
virtual std::size_t cancelGeneration(uint32_t generation) = 0;
|
||||
};
|
||||
|
||||
enum class MapTileReservationStatus : uint8_t
|
||||
{
|
||||
Reserved,
|
||||
Backpressured,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
enum class MapTileExecutionStatus : uint8_t
|
||||
{
|
||||
Completed,
|
||||
Backpressured,
|
||||
Cancelled,
|
||||
};
|
||||
|
||||
class IMapTileEventSink
|
||||
{
|
||||
public:
|
||||
virtual ~IMapTileEventSink() = default;
|
||||
|
||||
// Single producer: reserve before reading. A successful reservation makes
|
||||
// publication capacity-safe. false from publish means cancellation only,
|
||||
// not queue-full or transient lock contention. Payload copying is owned
|
||||
// by the adapter; allocation failure must publish a Failed completion.
|
||||
virtual MapTileReservationStatus reserve(const LoadTileCommand& command) = 0;
|
||||
virtual bool publish(const MapTileAsyncEvent& event) = 0;
|
||||
virtual void releaseReservation() = 0;
|
||||
};
|
||||
|
||||
class IMapTileWorkerBackend
|
||||
@@ -173,6 +227,8 @@ class MapTileAsyncRuntime
|
||||
explicit MapTileAsyncRuntime(IMapTileCommandSink& commands);
|
||||
|
||||
uint32_t activeGeneration() const;
|
||||
TileSubmitResult requestTile(const MapTileRef& tile, uint32_t generation,
|
||||
MapTileInteractionMode mode, uint32_t now_ms);
|
||||
std::size_t requestVisibleTiles(const MapViewportPlan& plan, uint32_t now_ms);
|
||||
std::size_t cancelGeneration(uint32_t generation);
|
||||
bool handleEvent(const MapTileAsyncEvent& event, MapTileRenderQueue& render_queue);
|
||||
@@ -202,6 +258,12 @@ class MapTileRuntime
|
||||
return runtime_.requestVisibleTiles(plan, now_ms);
|
||||
}
|
||||
|
||||
TileSubmitResult requestTile(const MapTileRef& tile, uint32_t generation,
|
||||
MapTileInteractionMode mode, uint32_t now_ms)
|
||||
{
|
||||
return runtime_.requestTile(tile, generation, mode, now_ms);
|
||||
}
|
||||
|
||||
std::size_t cancelGeneration(uint32_t generation)
|
||||
{
|
||||
state_machine_.cancelGeneration(generation);
|
||||
@@ -243,7 +305,7 @@ class MapTileWorker
|
||||
uint8_t* scratch,
|
||||
std::size_t scratch_size);
|
||||
|
||||
bool execute(const LoadTileCommand& command, uint32_t now_ms);
|
||||
MapTileExecutionStatus execute(const LoadTileCommand& command, uint32_t now_ms);
|
||||
|
||||
private:
|
||||
IMapTileWorkerBackend& backend_;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
namespace ui::map_tiles
|
||||
{
|
||||
// Bounded completion storage. The platform serializes calls; this component
|
||||
// deliberately knows nothing about RTOS locks, SD transports or payload heaps.
|
||||
// A reservation counts against capacity before any I/O is started. Cancelled
|
||||
// reservations remain occupied until their producer acknowledges cancellation.
|
||||
template <typename T, std::size_t Capacity>
|
||||
class MapTileCompletionQueue
|
||||
{
|
||||
static_assert(Capacity > 0, "A completion queue needs storage");
|
||||
|
||||
public:
|
||||
struct Reservation
|
||||
{
|
||||
std::size_t slot = Capacity;
|
||||
uint32_t epoch = 0;
|
||||
explicit operator bool() const { return slot < Capacity && epoch != 0; }
|
||||
};
|
||||
|
||||
MapTileCompletionQueue() = default;
|
||||
MapTileCompletionQueue(const MapTileCompletionQueue&) = delete;
|
||||
MapTileCompletionQueue& operator=(const MapTileCompletionQueue&) = delete;
|
||||
|
||||
Reservation reserve(uint32_t generation)
|
||||
{
|
||||
for (std::size_t i = 0; i < Capacity; ++i)
|
||||
{
|
||||
auto& slot = slots_[i];
|
||||
if (slot.state != State::Free) continue;
|
||||
if (++slot.epoch == 0) ++slot.epoch;
|
||||
slot.generation = generation;
|
||||
slot.state = State::Reserved;
|
||||
++occupied_;
|
||||
if (occupied_ > high_water_) high_water_ = occupied_;
|
||||
return {i, slot.epoch};
|
||||
}
|
||||
++backpressure_count_;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool active(Reservation token) const
|
||||
{
|
||||
return matches(token) && slots_[token.slot].state == State::Reserved;
|
||||
}
|
||||
|
||||
// false means cancellation (or a stale token), never queue-full. On false
|
||||
// the caller retains payload ownership and must release its reservation.
|
||||
bool commit(Reservation token, T&& value)
|
||||
{
|
||||
if (!active(token)) return false;
|
||||
auto& slot = slots_[token.slot];
|
||||
slot.value = std::move(value);
|
||||
slot.sequence = next_sequence_++;
|
||||
slot.state = State::Ready;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Only the producer releases a reservation. This cannot release a newer
|
||||
// reservation after its slot has been recycled (epoch/ABA protection).
|
||||
bool release(Reservation token)
|
||||
{
|
||||
if (!matches(token)) return false;
|
||||
auto& slot = slots_[token.slot];
|
||||
if (slot.state != State::Reserved && slot.state != State::Cancelled) return false;
|
||||
slot.state = State::Free;
|
||||
--occupied_;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t cancelGeneration(uint32_t generation)
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (auto& slot : slots_)
|
||||
{
|
||||
if (slot.generation != generation) continue;
|
||||
if (slot.state == State::Reserved)
|
||||
{
|
||||
slot.state = State::Cancelled;
|
||||
++count;
|
||||
}
|
||||
else if (slot.state == State::Ready)
|
||||
{
|
||||
slot.state = State::Discarded;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Discarded payloads are moved to the caller, so expensive heap release
|
||||
// can happen outside the queue lock. They do not consume render budget.
|
||||
bool takeDiscarded(T& out) { return take(State::Discarded, out); }
|
||||
bool pop(T& out) { return take(State::Ready, out); }
|
||||
|
||||
template <typename Predicate>
|
||||
bool popIf(T& out, Predicate eligible)
|
||||
{
|
||||
std::size_t index = Capacity;
|
||||
for (std::size_t i = 0; i < Capacity; ++i)
|
||||
{
|
||||
if (slots_[i].state == State::Ready && eligible(slots_[i].value) &&
|
||||
(index == Capacity || static_cast<int32_t>(slots_[i].sequence - slots_[index].sequence) < 0)) index = i;
|
||||
}
|
||||
if (index == Capacity) return false;
|
||||
out = std::move(slots_[index].value);
|
||||
slots_[index].value = T{};
|
||||
slots_[index].state = State::Free;
|
||||
--occupied_;
|
||||
return true;
|
||||
}
|
||||
|
||||
const T* front() const
|
||||
{
|
||||
const auto index = oldest(State::Ready);
|
||||
return index == Capacity ? nullptr : &slots_[index].value;
|
||||
}
|
||||
|
||||
std::size_t occupied() const { return occupied_; }
|
||||
std::size_t highWater() const { return high_water_; }
|
||||
uint32_t backpressureCount() const { return backpressure_count_; }
|
||||
|
||||
private:
|
||||
enum class State : uint8_t
|
||||
{
|
||||
Free,
|
||||
Reserved,
|
||||
Ready,
|
||||
Cancelled,
|
||||
Discarded
|
||||
};
|
||||
struct Slot
|
||||
{
|
||||
T value{};
|
||||
uint32_t generation = 0;
|
||||
uint32_t epoch = 0;
|
||||
uint32_t sequence = 0;
|
||||
State state = State::Free;
|
||||
};
|
||||
|
||||
bool matches(Reservation token) const
|
||||
{
|
||||
return token && slots_[token.slot].epoch == token.epoch;
|
||||
}
|
||||
|
||||
std::size_t oldest(State state) const
|
||||
{
|
||||
std::size_t found = Capacity;
|
||||
for (std::size_t i = 0; i < Capacity; ++i)
|
||||
{
|
||||
if (slots_[i].state == state &&
|
||||
(found == Capacity || static_cast<int32_t>(slots_[i].sequence - slots_[found].sequence) < 0))
|
||||
found = i;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
bool take(State state, T& out)
|
||||
{
|
||||
const auto index = oldest(state);
|
||||
if (index == Capacity) return false;
|
||||
auto& slot = slots_[index];
|
||||
out = std::move(slot.value);
|
||||
slot.value = T{};
|
||||
slot.state = State::Free;
|
||||
--occupied_;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<Slot, Capacity> slots_{};
|
||||
std::size_t occupied_ = 0;
|
||||
std::size_t high_water_ = 0;
|
||||
uint32_t next_sequence_ = 0;
|
||||
uint32_t backpressure_count_ = 0;
|
||||
};
|
||||
} // namespace ui::map_tiles
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_map_runtime/map_tiles/map_tile_async_runtime.h"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui::map_tiles
|
||||
{
|
||||
// UI-thread-owned, bounded summaries. Worker evidence travels with completion
|
||||
// ownership; no shared mutable worker/UI counters and no per-sector logging.
|
||||
class MapTilePipelineMetrics
|
||||
{
|
||||
public:
|
||||
enum Stage : unsigned
|
||||
{
|
||||
CommandWait,
|
||||
Worker,
|
||||
FileLock,
|
||||
FileOpen,
|
||||
FileRead,
|
||||
ResultWait,
|
||||
Decode,
|
||||
Apply,
|
||||
StageCount
|
||||
};
|
||||
struct Sample
|
||||
{
|
||||
uint64_t total_ms = 0;
|
||||
uint32_t count = 0;
|
||||
uint32_t max_ms = 0;
|
||||
void add(uint32_t ms)
|
||||
{
|
||||
total_ms += ms;
|
||||
++count;
|
||||
if (ms > max_ms) max_ms = ms;
|
||||
}
|
||||
uint32_t average() const { return count ? static_cast<uint32_t>(total_ms / count) : 0; }
|
||||
};
|
||||
|
||||
void consumed(const MapTileAsyncEvent& event, uint32_t now_ms)
|
||||
{
|
||||
++consumed_count;
|
||||
if (event.kind == MapTileAsyncEventKind::Ready) ++ready_count;
|
||||
else if (event.kind == MapTileAsyncEventKind::RetryLater) ++retry_count;
|
||||
else ++failed_count;
|
||||
if (event.timing_available)
|
||||
{
|
||||
stages[CommandWait].add(event.command_wait_ms);
|
||||
stages[Worker].add(event.published_ms - event.worker_started_ms);
|
||||
stages[ResultWait].add(now_ms - event.published_ms);
|
||||
}
|
||||
if (event.read_timing.available)
|
||||
{
|
||||
stages[FileLock].add(event.read_timing.lock_wait_ms);
|
||||
stages[FileOpen].add(event.read_timing.open_ms);
|
||||
stages[FileRead].add(event.read_timing.read_ms);
|
||||
}
|
||||
if (event.read_timing.block_available)
|
||||
{
|
||||
++block_samples;
|
||||
block_calls += event.read_timing.block_calls;
|
||||
block_sectors += event.read_timing.block_sectors;
|
||||
block_us += event.read_timing.block_us;
|
||||
if (event.read_timing.block_max_sectors > block_max_sectors)
|
||||
block_max_sectors = event.read_timing.block_max_sectors;
|
||||
}
|
||||
}
|
||||
|
||||
std::array<Sample, StageCount> stages{};
|
||||
uint32_t consumed_count = 0;
|
||||
uint32_t ready_count = 0;
|
||||
uint32_t retry_count = 0;
|
||||
uint32_t failed_count = 0;
|
||||
uint32_t rendered_count = 0;
|
||||
uint32_t submission_backpressure = 0;
|
||||
uint32_t block_samples = 0;
|
||||
uint64_t block_calls = 0;
|
||||
uint64_t block_sectors = 0;
|
||||
uint64_t block_us = 0;
|
||||
uint32_t block_max_sectors = 0;
|
||||
};
|
||||
} // namespace ui::map_tiles
|
||||
@@ -0,0 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_map_runtime/map_tiles/map_tile_async_runtime.h"
|
||||
#include <array>
|
||||
|
||||
namespace ui::map_tiles
|
||||
{
|
||||
// A command slot remains owned until its completion is consumed or it is
|
||||
// cancelled. This is both the dedupe index and the bounded in-flight ledger;
|
||||
// no second unbounded registry is needed. Platform owns synchronization.
|
||||
template <std::size_t Capacity>
|
||||
class MapTileRequestQueue
|
||||
{
|
||||
public:
|
||||
void statistics(std::size_t& queued, std::size_t& in_flight) const
|
||||
{
|
||||
queued = in_flight = 0;
|
||||
for (const auto& slot : slots_)
|
||||
{
|
||||
if (slot.state == State::Queued) ++queued;
|
||||
if (slot.state == State::InFlight) ++in_flight;
|
||||
}
|
||||
}
|
||||
|
||||
TileSubmitResult enqueue(const LoadTileCommand& command)
|
||||
{
|
||||
Slot* free = nullptr;
|
||||
for (auto& slot : slots_)
|
||||
{
|
||||
if (slot.state == State::Free)
|
||||
{
|
||||
if (!free) free = &slot;
|
||||
continue;
|
||||
}
|
||||
if (slot.command.runtime.generation == command.runtime.generation && sameTile(slot.command.tile, command.tile))
|
||||
return {TileSubmitStatus::AlreadyPending, handle(slot.command)};
|
||||
}
|
||||
if (!free) return {TileSubmitStatus::Backpressured, {}};
|
||||
free->command = command;
|
||||
free->state = State::Queued;
|
||||
return {TileSubmitStatus::Accepted, handle(command)};
|
||||
}
|
||||
|
||||
bool pop(uint32_t now_ms, LoadTileCommand& out)
|
||||
{
|
||||
(void)now_ms; // Tile requests currently have no deadlines; cancellation is explicit.
|
||||
Slot* best = nullptr;
|
||||
for (auto& slot : slots_)
|
||||
{
|
||||
if (slot.state != State::Queued) continue;
|
||||
if (!best || slot.command.runtime.priority < best->command.runtime.priority ||
|
||||
(slot.command.runtime.priority == best->command.runtime.priority &&
|
||||
static_cast<int32_t>(slot.command.runtime.created_at_ms - best->command.runtime.created_at_ms) < 0))
|
||||
best = &slot;
|
||||
}
|
||||
if (!best) return false;
|
||||
best->state = State::InFlight;
|
||||
out = best->command;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool complete(TileRequestHandle request)
|
||||
{
|
||||
for (auto& slot : slots_)
|
||||
{
|
||||
if (slot.state == State::InFlight && request.matches(slot.command.runtime.generation, slot.command.runtime.command_id))
|
||||
{
|
||||
slot.state = State::Free;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::size_t cancelGeneration(uint32_t generation)
|
||||
{
|
||||
std::size_t removed = 0;
|
||||
for (auto& slot : slots_)
|
||||
{
|
||||
if (slot.state != State::Free && slot.command.runtime.generation == generation)
|
||||
{
|
||||
slot.state = State::Free;
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private:
|
||||
static TileRequestHandle handle(const LoadTileCommand& command)
|
||||
{
|
||||
return {command.runtime.generation, command.runtime.command_id};
|
||||
}
|
||||
static bool sameTile(const MapTileRef& left, const MapTileRef& right)
|
||||
{
|
||||
return left.layer == right.layer && left.z == right.z && left.x == right.x && left.y == right.y;
|
||||
}
|
||||
enum class State : uint8_t
|
||||
{
|
||||
Free,
|
||||
Queued,
|
||||
InFlight
|
||||
};
|
||||
struct Slot
|
||||
{
|
||||
LoadTileCommand command{};
|
||||
State state = State::Free;
|
||||
};
|
||||
std::array<Slot, Capacity> slots_{};
|
||||
};
|
||||
} // namespace ui::map_tiles
|
||||
@@ -19,12 +19,28 @@ enum class MapTileReadStatus : uint8_t
|
||||
Invalid,
|
||||
};
|
||||
|
||||
// Optional file-stage evidence. Cached/generated sources leave available false;
|
||||
// zero milliseconds on a measured stage is a valid sub-tick measurement.
|
||||
struct MapTileReadTiming
|
||||
{
|
||||
uint32_t lock_wait_ms = 0;
|
||||
uint32_t open_ms = 0;
|
||||
uint32_t read_ms = 0;
|
||||
bool available = false;
|
||||
uint32_t block_calls = 0;
|
||||
uint32_t block_sectors = 0;
|
||||
uint32_t block_max_sectors = 0;
|
||||
uint32_t block_us = 0;
|
||||
bool block_available = false;
|
||||
};
|
||||
|
||||
struct MapTileReadResult
|
||||
{
|
||||
MapTileReadStatus status = MapTileReadStatus::Error;
|
||||
std::size_t size = 0;
|
||||
int32_t error = -1;
|
||||
MapTileFormat format = MapTileFormat::Unknown;
|
||||
MapTileReadTiming timing{};
|
||||
};
|
||||
|
||||
class IMapTileSource
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui_map_runtime/map_tiles/map_tile_async_runtime.h"
|
||||
#include <utility>
|
||||
|
||||
namespace ui::map_tiles
|
||||
{
|
||||
// Platform-provided deleter keeps heap/PSRAM policy outside the map domain.
|
||||
class OwnedMapTileEvent
|
||||
{
|
||||
public:
|
||||
using Deleter = void (*)(MapTileAsyncEvent&);
|
||||
OwnedMapTileEvent() = default;
|
||||
OwnedMapTileEvent(MapTileAsyncEvent event, Deleter deleter)
|
||||
: event_(event), deleter_(deleter) {}
|
||||
~OwnedMapTileEvent() { reset(); }
|
||||
OwnedMapTileEvent(const OwnedMapTileEvent&) = delete;
|
||||
OwnedMapTileEvent& operator=(const OwnedMapTileEvent&) = delete;
|
||||
OwnedMapTileEvent(OwnedMapTileEvent&& other) noexcept
|
||||
: event_(other.release()), deleter_(other.deleter_) {}
|
||||
OwnedMapTileEvent& operator=(OwnedMapTileEvent&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
reset();
|
||||
event_ = other.release();
|
||||
deleter_ = other.deleter_;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
const MapTileAsyncEvent& get() const { return event_; }
|
||||
MapTileAsyncEvent release()
|
||||
{
|
||||
auto result = event_;
|
||||
event_ = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
void reset()
|
||||
{
|
||||
if (deleter_ && event_.payload.data) deleter_(event_);
|
||||
event_ = {};
|
||||
}
|
||||
MapTileAsyncEvent event_{};
|
||||
Deleter deleter_ = nullptr;
|
||||
};
|
||||
} // namespace ui::map_tiles
|
||||
Reference in new issue
Block a user