Files
mrgameeng/samples/MrGameEng.Sample/SampleSystems.cs
T
Leonid PershinandClaude Fable 5 e06f24a319 Parallel rendering pipeline, ring vertex buffer, phase timings
Five optimizations measured on the 100k-entity stress scene (Release,
vsync off): 103 FPS baseline -> 297 FPS.

- Sprite submission and vertex building run on all cores above
  Renderer2DOptions.ParallelThreshold (default 8192). Work is sliced
  into 4096-entity segments: a Friflo chunk holds a whole archetype,
  so per-chunk parallelism degenerates to one thread. Segments merge
  in deterministic order, preserving radix sort stability.
- Vertex buffer is ring-written with SetDataOptions.NoOverwrite
  (GPU buffer 2x frame size); Discard only on wrap-around.
- Texture2DRegion precomputes UVs - four float divisions per sprite
  per frame removed.
- Renderer2D exposes per-phase timings (submit/sort/build/upload/draw),
  shown in the sample HUD - all further optimization is data-driven.
- Sample BounceSystem parallelized the same segmented way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 05:06:55 +03:00

209 lines
7.5 KiB
C#

using Friflo.Engine.ECS;
using Friflo.Engine.ECS.Systems;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Input;
using MrGameEng.Audio;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Input;
namespace MrGameEng.Sample;
/// <summary>Управление игроком (WASD/стрелки) и прыжок-писк на пробел.</summary>
public sealed class PlayerControlSystem(
Entity player, ActionMap<SampleAction> actions, AudioManager audio, SoundEffect beep) : BaseSystem
{
protected override void OnUpdateGroup()
{
ref var transform = ref player.GetComponent<Transform2D>();
var move = new Vector2(
actions.GetAxis(SampleAction.MoveLeft, SampleAction.MoveRight),
actions.GetAxis(SampleAction.MoveUp, SampleAction.MoveDown));
if (move != Vector2.Zero)
{
move.Normalize();
transform.Position += move * 260f * Tick.deltaTime;
}
if (actions.IsPressed(SampleAction.Jump))
{
audio.Play(beep);
}
}
}
/// <summary>Камера следует за игроком; колесо — зум, Q/E — поворот.</summary>
public sealed class CameraControlSystem(Entity cameraEntity, Entity player, InputManager input) : BaseSystem
{
protected override void OnUpdateGroup()
{
ref var camera = ref cameraEntity.GetComponent<Camera>();
var target = player.GetComponent<Transform2D>().Position;
camera.Position = Vector2.Lerp(camera.Position, target, Math.Min(1f, 6f * Tick.deltaTime));
camera.Zoom = Math.Clamp(camera.Zoom * (1f + input.WheelDelta * 0.001f), 0.2f, 5f);
var rotate = (input.IsKeyDown(Keys.E) ? 1f : 0f) - (input.IsKeyDown(Keys.Q) ? 1f : 0f);
camera.Rotation += rotate * 1.2f * Tick.deltaTime;
}
}
/// <summary>
/// Отскок сущностей со скоростью от границ мира. На больших количествах работа режется
/// на сегменты по всем ядрам (чанк Friflo держит весь архетип — сам по себе он слишком крупный).
/// </summary>
public sealed class BounceSystem(RectF bounds) : QuerySystem<Transform2D, Velocity>
{
private const int ParallelThreshold = 8192;
private const int SegmentSize = 8192;
private readonly List<(Chunk<Transform2D> Transforms, Chunk<Velocity> Velocities)> _chunks = [];
private readonly List<(int Chunk, int Start, int Length)> _segments = [];
protected override void OnUpdate()
{
var delta = Tick.deltaTime;
_chunks.Clear();
var total = 0;
foreach (var (transforms, velocities, _) in Query.Chunks)
{
_chunks.Add((transforms, velocities));
total += transforms.Length;
}
if (total < ParallelThreshold)
{
foreach (var (transforms, velocities) in _chunks)
{
Move(transforms, velocities, 0, transforms.Length, delta);
}
return;
}
_segments.Clear();
for (var c = 0; c < _chunks.Count; c++)
{
var length = _chunks[c].Transforms.Length;
for (var start = 0; start < length; start += SegmentSize)
{
_segments.Add((c, start, Math.Min(SegmentSize, length - start)));
}
}
Parallel.For(0, _segments.Count, i =>
{
var (chunk, start, length) = _segments[i];
Move(_chunks[chunk].Transforms, _chunks[chunk].Velocities, start, length, delta);
});
}
private void Move(Chunk<Transform2D> transforms, Chunk<Velocity> velocities, int start, int length, float delta)
{
var t = transforms.Span.Slice(start, length);
var v = velocities.Span.Slice(start, length);
for (var i = 0; i < t.Length; i++)
{
ref var position = ref t[i].Position;
ref var velocity = ref v[i].Value;
position += velocity * delta;
if (position.X < bounds.Left || position.X > bounds.Right)
{
velocity.X = -velocity.X;
position.X = Math.Clamp(position.X, bounds.Left, bounds.Right);
}
if (position.Y < bounds.Top || position.Y > bounds.Bottom)
{
velocity.Y = -velocity.Y;
position.Y = Math.Clamp(position.Y, bounds.Top, bounds.Bottom);
}
}
}
}
/// <summary>Пауза (P), музыка (M), переключение сцены с переходом (Tab).</summary>
public sealed class SceneHotkeysSystem(
EngineContext context, ActionMap<SampleAction> actions, Func<Scene> nextScene, Transition transition) : BaseSystem
{
protected override void OnUpdateGroup()
{
if (actions.IsPressed(SampleAction.Pause))
{
context.Clock.TimeScale = context.Clock.TimeScale > 0f ? 0f : 1f;
}
if (actions.IsPressed(SampleAction.ToggleMusic))
{
var music = context.Services.Get<AudioManager>().Music;
if (music.IsPlaying)
{
music.Pause();
}
else
{
music.Resume();
}
}
if (actions.IsPressed(SampleAction.SwitchScene) && !context.Scenes.IsTransitioning)
{
context.Scenes.Switch(nextScene(), transition);
}
}
}
/// <summary>FPS и статистика рендера: в HUD-лейбл и в заголовок окна (4 раза в секунду).</summary>
public sealed class StatsSystem(
EngineContext context, Renderer2D renderer, string sceneName, Myra.Graphics2D.UI.Label? hudLabel = null) : BaseSystem
{
private float _accumulated;
private int _frames;
protected override void OnUpdateGroup()
{
_accumulated += context.Clock.UnscaledDeltaTime;
_frames++;
if (_accumulated < 0.25f)
{
return;
}
var fps = _frames / _accumulated;
_accumulated = 0f;
_frames = 0;
var stats =
$"{fps:F0} FPS | sprites: {renderer.SubmittedSprites} | culled: {renderer.CulledSprites} | draw calls: {renderer.DrawCalls}";
context.Services.Get<GameWindow>().Title = $"MrGameEng Sample — {sceneName} | {stats}";
if (hudLabel is not null)
{
hudLabel.Text =
$"{sceneName}\n{stats}\n" +
$"submit {renderer.SubmitMs:F2} | sort {renderer.SortMs:F2} | build {renderer.BuildMs:F2} | " +
$"upload {renderer.UploadMs:F2} | draw {renderer.DrawMs:F2} (ms)";
}
}
}
/// <summary>Общая настройка ввода для сцен сэмпла.</summary>
public static class SampleInput
{
public static ActionMap<SampleAction> CreateActions(InputManager input) =>
new ActionMap<SampleAction>(input)
.Bind(SampleAction.MoveLeft, Keys.A)
.Bind(SampleAction.MoveLeft, Keys.Left)
.Bind(SampleAction.MoveRight, Keys.D)
.Bind(SampleAction.MoveRight, Keys.Right)
.Bind(SampleAction.MoveUp, Keys.W)
.Bind(SampleAction.MoveUp, Keys.Up)
.Bind(SampleAction.MoveDown, Keys.S)
.Bind(SampleAction.MoveDown, Keys.Down)
.Bind(SampleAction.Jump, Keys.Space)
.Bind(SampleAction.Pause, Keys.P)
.Bind(SampleAction.ToggleMusic, Keys.M)
.Bind(SampleAction.SwitchScene, Keys.Tab);
}