Prepare 0.1.48-alpha: optimize SD transports and repair map lifecycle

This commit is contained in:
liu weikai committed 2026-09-20 18:00:31 +08:00
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