wip new display module

This commit is contained in:
Vinzenz Schroeter 2024-04-12 14:29:26 +02:00
parent 7e767d6dcb
commit 38463ac109
14 changed files with 321 additions and 1 deletions

View file

@ -0,0 +1,25 @@
using System.Diagnostics;
namespace DisplayCommands;
public class ByteGrid(ushort width, ushort height)
{
public ushort Height { get; } = height;
public ushort Width { get; } = width;
internal Memory<byte> Data { get; } = new byte[width * height].AsMemory();
public byte this[ushort x, ushort y]
{
get => Data.Span[ GetIndex(x, y)];
set => Data.Span[GetIndex(x, y)] = value;
}
private int GetIndex(ushort x, ushort y)
{
Debug.Assert(x < Width);
Debug.Assert(y < Height);
return x + y * Width;
}
}

View file

@ -0,0 +1,59 @@
using System.Diagnostics;
using System.Text;
namespace DisplayCommands;
public sealed class Cp437Grid(ushort width, ushort height)
{
private readonly ByteGrid _byteGrid = new(width, height);
public ushort Height { get; } = height;
public ushort Width { get; } = width;
internal Memory<byte> Data => _byteGrid.Data;
private readonly Encoding _encoding = Encoding.GetEncoding(437);
public char this[ushort x, ushort y]
{
get => ByteToChar(_byteGrid[x, y]);
set => _byteGrid[x, y] = CharToByte(value);
}
public string this[ushort row]
{
get
{
var rowStart = row * Width;
return _encoding.GetString(_byteGrid.Data[rowStart..(rowStart + Width)].Span);
}
set
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(row, Height, nameof(row));
ArgumentOutOfRangeException.ThrowIfGreaterThan(value.Length, Width, nameof(value));
ushort x = 0;
for (; x < value.Length; x++)
_byteGrid[x, row] = CharToByte(value[x]);
for (; x < Width; x++)
_byteGrid[x, row] = CharToByte(' ');
}
}
private byte CharToByte(char c)
{
ReadOnlySpan<char> valuesStr = stackalloc char[] { c };
Span<byte> convertedStr = stackalloc byte[1];
var consumed = _encoding.GetBytes(valuesStr, convertedStr);
Debug.Assert(consumed == 1);
return convertedStr[0];
}
private char ByteToChar(byte b)
{
ReadOnlySpan<byte> valueBytes = stackalloc byte[] { b };
Span<char> resultStr = stackalloc char[1];
_encoding.GetChars(valueBytes, resultStr);
return resultStr[0];
}
}

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IsAotCompatible>true</IsAotCompatible>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
<PropertyGroup>
<AnalysisMode>Recommended</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>CA1805,CA1848</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,8 @@
namespace DisplayCommands;
public class DisplayConfiguration
{
public string Hostname { get; set; } = "172.23.42.29";
public int Port { get; set; } = 2342;
}

View file

@ -0,0 +1,25 @@
using System.Text;
using DisplayCommands.Internals;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DisplayCommands;
public static class DisplayExtensions
{
static DisplayExtensions()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public static IServiceCollection AddDisplay(
this IServiceCollection services,
IConfigurationSection? configurationSection = null
)
{
services.AddSingleton<IDisplayConnection, DisplayConnection>();
if (configurationSection != null)
services.Configure<DisplayConfiguration>(configurationSection);
return services;
}
}

View file

@ -0,0 +1,4 @@
// Global using directives
global using System;
global using System.Threading.Tasks;

View file

@ -0,0 +1,13 @@
namespace DisplayCommands;
public interface IDisplayConnection
{
ValueTask SendClearAsync();
ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid);
ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma);
ValueTask SendBrightnessAsync(byte brightness);
ValueTask SendHardResetAsync();
ValueTask SendFadeOutAsync(byte loops);
}

View file

@ -0,0 +1,17 @@
namespace DisplayCommands.Internals;
internal enum DisplayCommand: ushort
{
Clear = 0x0002,
Cp437Data = 0x0003,
CharBrightness = 0x0005,
Brightness = 0x0007,
HardReset = 0x000b,
FadeOut = 0x000d,
BitmapLegacy = 0x0010,
BitmapLinear = 0x0012,
BitmapLinearWin = 0x0013,
BitmapLinearAnd = 0x0014,
BitmapLinearOr = 0x0015,
BitmapLinearXor = 0x0016,
}

View file

@ -0,0 +1,104 @@
using System.Buffers;
using System.Diagnostics;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Options;
namespace DisplayCommands.Internals;
internal sealed class DisplayConnection(IOptions<DisplayConfiguration> options) : IDisplayConnection, IDisposable
{
private readonly UdpClient _udpClient = new(options.Value.Hostname, options.Value.Port);
private readonly ArrayPool<byte> _arrayPool = ArrayPool<byte>.Shared;
public ValueTask SendClearAsync()
{
var header = new HeaderWindow { Command = DisplayCommand.Clear };
return SendAsync(header, Memory<byte>.Empty);
}
public ValueTask SendCp437DataAsync(ushort x, ushort y, Cp437Grid grid)
{
var header = new HeaderWindow
{
Command = DisplayCommand.Cp437Data,
Height = grid.Height,
Width = grid.Width,
PosX = x,
PosY = y
};
return SendAsync(header, grid.Data);
}
public ValueTask SendCharBrightnessAsync(ushort x, ushort y, ByteGrid luma)
{
var header = new HeaderWindow
{
Command = DisplayCommand.CharBrightness,
PosX = x,
PosY = y,
Height = luma.Height,
Width = luma.Width
};
return SendAsync(header, luma.Data);
}
public async ValueTask SendBrightnessAsync(byte brightness)
{
var header = new HeaderWindow { Command = DisplayCommand.Brightness };
var payloadBuffer = _arrayPool.Rent(1);
var payload = payloadBuffer.AsMemory(0, 1);
payload.Span[0] = brightness;
await SendAsync(header, payload);
_arrayPool.Return(payloadBuffer);
}
public ValueTask SendHardResetAsync()
{
var header = new HeaderWindow { Command = DisplayCommand.HardReset };
return SendAsync(header, Memory<byte>.Empty);
}
public async ValueTask SendFadeOutAsync(byte loops)
{
var header = new HeaderWindow { Command = DisplayCommand.FadeOut };
var payloadBuffer = _arrayPool.Rent(1);
var payload = payloadBuffer.AsMemory(0, 1);
payload.Span[0] = loops;
await SendAsync(header, payload);
_arrayPool.Return(payloadBuffer);
}
private async ValueTask SendAsync(HeaderWindow header, Memory<byte> payload)
{
int headerSize;
unsafe
{
// because we specified the struct layout, no platform-specific padding will be added and this is be safe.
headerSize = sizeof(HeaderWindow);
}
Debug.Assert(headerSize == 10);
var messageSize = headerSize + payload.Length;
var buffer = _arrayPool.Rent(messageSize);
var message = buffer.AsMemory(0, messageSize);
MemoryMarshal.Write(message.Span, header);
payload.CopyTo(message[headerSize..]);
await _udpClient.SendAsync(message);
_arrayPool.Return(buffer);
}
public void Dispose()
{
_udpClient.Dispose();
}
}

View file

@ -0,0 +1,10 @@
namespace DisplayCommands.Internals;
internal enum DisplaySubCommand
{
BitmapNormal = 0x0,
BitmapCompressZ = 0x677a,
BitmapCompressBz = 0x627a,
BitmapCompressLz = 0x6c7a,
BitmapCompressZs = 0x7a73,
}

View file

@ -0,0 +1,17 @@
using System.Runtime.InteropServices;
namespace DisplayCommands.Internals;
[StructLayout(LayoutKind.Sequential, Pack = 16, Size = 10)]
internal struct HeaderWindow
{
public DisplayCommand Command;
public ushort PosX;
public ushort PosY;
public ushort Width;
public ushort Height;
}

View file

@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TanksServer", "TanksServer\TanksServer.csproj", "{D88BF376-47A4-4C72-ADD1-983F9285C351}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DisplayCommands", "DisplayCommands\DisplayCommands.csproj", "{B4B43561-7A2C-486B-99F7-E58A67BC370A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -12,5 +14,9 @@ Global
{D88BF376-47A4-4C72-ADD1-983F9285C351}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D88BF376-47A4-4C72-ADD1-983F9285C351}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D88BF376-47A4-4C72-ADD1-983F9285C351}.Release|Any CPU.Build.0 = Release|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4B43561-7A2C-486B-99F7-E58A67BC370A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,5 +1,5 @@
using System.IO;
using System.Xml;
using DisplayCommands;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -115,6 +115,7 @@ public static class Program
builder.Configuration.GetSection("Tanks"));
builder.Services.Configure<PlayersConfiguration>(
builder.Configuration.GetSection("Players"));
builder.Services.AddDisplay(builder.Configuration.GetSection("ServicePointDisplay"));
var app = builder.Build();

View file

@ -42,4 +42,8 @@
<Content Include="../Makefile" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\DisplayCommands\DisplayCommands.csproj" />
</ItemGroup>
</Project>