diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..bbf1d9fc0 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + + + diff --git a/Game.Client/IClientLeafSystem.cs b/Game.Client/IClientLeafSystem.cs index 631425008..ad0a4b1e8 100644 --- a/Game.Client/IClientLeafSystem.cs +++ b/Game.Client/IClientLeafSystem.cs @@ -5,6 +5,7 @@ using Source.Common.Engine; using System.Numerics; +using System.Runtime.InteropServices; namespace Game.Client; @@ -32,7 +33,11 @@ public struct Entry public readonly int[] RenderGroupCounts = new int[RENDER_GROUP_COUNT]; public void Init() { - Array.Clear(RenderGroups); + for (int i = 0; i < RENDER_GROUP_COUNT; i++) { + int count = RenderGroupCounts[i]; + if (count > 0) + MemoryMarshal.CreateSpan(ref RenderGroups[i, 0], count).Clear(); + } Array.Clear(RenderGroupCounts); } diff --git a/Game.Server/AI_BaseNPC.cs b/Game.Server/AI_BaseNPC.cs index f5c5c6ba8..09b151355 100644 --- a/Game.Server/AI_BaseNPC.cs +++ b/Game.Server/AI_BaseNPC.cs @@ -6674,11 +6674,11 @@ public void CheckTarget(BaseEntity target) { UpdateTargetPos(); } - public void RememberUnreachable(BaseEntity entity, float duration = -1) { + public void RememberUnreachable(BaseEntity entity, TimeUnit_t duration = -1) { if (entity == GetEnemy()) ForceChooseNewEnemy(); - float NPC_UNREACHABLE_TIMEOUT = (duration > 0.0) ? duration : 3; + TimeUnit_t NPC_UNREACHABLE_TIMEOUT = (duration > 0.0) ? duration : 3; for (int i = UnreachableEnts.Count - 1; i >= 0; i--) { if (entity == UnreachableEnts[i].UnreachableEntity.Get()) { UnreachableEnt existing = UnreachableEnts[i]; diff --git a/Game.Server/BaseFlex.cs b/Game.Server/BaseFlex.cs index f5cccaad5..49b4a48fb 100644 --- a/Game.Server/BaseFlex.cs +++ b/Game.Server/BaseFlex.cs @@ -13,12 +13,14 @@ using System.Numerics; namespace Game.Server; + using FIELD = Source.FIELD; using EventType = Game.Shared.ChoreoEvent.EventType; [LinkEntityToClass("funCBaseFlex")] [NetworkName("CBaseFlex")] -public class BaseFlex : BaseAnimatingOverlay { +public class BaseFlex : BaseAnimatingOverlay +{ public static readonly SendTable DT_BaseFlex = new(DT_BaseAnimatingOverlay, [ SendPropArray3 (FIELD.OF_ARRAY(nameof(FlexWeight)), SendPropFloat(FIELD.OF_ARRAY(nameof(FlexWeight)), 12, PropFlags.RoundDown, 0.0f, 1.0f )), SendPropInt (FIELD.OF(nameof(BlinkToggle)), 1, PropFlags.Unsigned ), @@ -286,7 +288,7 @@ public void AddSceneEvent(ChoreoScene? scene, ChoreoEvent? ev, BaseEntity? targe ChoreoActor? actor = ev.GetActor(); if (actor == null) { - Msg("CBaseFlex::AddSceneEvent: event->GetActor() was NULL!!!\n"); + Msg("CBaseFlex::AddSceneEvent: event.GetActor() was NULL!!!\n"); return; } @@ -434,7 +436,7 @@ bool HandleStartGestureSceneEvent(SceneEventInfo info, ChoreoScene scene, Choreo EventAbsoluteTag? origTag = ev.FindAbsoluteTag(ChoreoEvent.AbsTagType.Original, kvTags.Name); if (origTag != null) { if (MathF.Abs(origTag.GetPercentage() - percentage) > 0.05) { - DevWarning($"{scene.GetFilename()} repositioned tag: {kvTags.Name} : {origTag.GetPercentage():F3} -> {percentage:F3} ({scene.GetFilename()}:{actor.GetName()}:{ev.GetParameters()})\n"); + DevWarning($"{scene.GetFilename()} repositioned tag: {kvTags.Name} : {origTag.GetPercentage():F3} . {percentage:F3} ({scene.GetFilename()}:{actor.GetName()}:{ev.GetParameters()})\n"); origTag.SetPercentage(percentage); } } @@ -830,10 +832,31 @@ bool ProcessMoveToSceneEvent(SceneEventInfo info, ChoreoScene scene, ChoreoEvent if (ev.GetForceShortMovement()) distTolerance = 0.1f; + AI_NavGoal goal = new AI_NavGoal(NavGoalType.TargetEnt, moveActivity, distTolerance, AI_NavGoalFlags.UpdateTargetPos); float dist = (info.Target.Get()!.EyePosition() - GetAbsOrigin()).Length2D(); - if (dist > Math.Max(Math.Max(distTolerance, 0.1f), ev.GetDistanceToTarget())) - throw new NotImplementedException(); + if (dist > Math.Max(Math.Max(distTolerance, 0.1f), ev.GetDistanceToTarget())) { + bool result = false; + + if (!myNpc.IsUnreachable(info.Target.Get()!)) { + result = myNpc.GetNavigator()!.SetGoal(in goal, AI_NavSetGoalFlags.ClearTarget); + if (!result) + myNpc.RememberUnreachable(info.Target.Get()!, 1.5); + } + + if (result) { + myNpc.GetNavigator()!.SetMovementActivity(moveActivity); + myNpc.GetNavigator()!.SetArrivalDistance(ev.GetDistanceToTarget()); + info.IsMoving = true; + } + else { + if (developer.GetInt() > 0 && scene_showmoveto.GetBool()) { + myNpc.GetMoveProbe()!.FloorPoint(info.Target.Get()!.EyePosition(), Mask.NPCSolid, 0, -64, out Vector3 testPoint); + DebugOverlay.HorzArrow(GetAbsOrigin() + new Vector3(0, 0, 1), testPoint + new Vector3(0, 0, 1), 4, 255, 0, 255, 0, false, 0.12f); + DebugOverlay.Box(testPoint, myNpc.GetHullMins(), myNpc.GetHullMaxs(), 255, 0, 255, 0, 0.12f); + } + } + } else info.HasArrived = true; } diff --git a/Source.CodeAnalysis.ConVars/ConVarRegistrationGenerator.cs b/Source.CodeAnalysis.ConVars/ConVarRegistrationGenerator.cs new file mode 100644 index 000000000..e3b205d2d --- /dev/null +++ b/Source.CodeAnalysis.ConVars/ConVarRegistrationGenerator.cs @@ -0,0 +1,528 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Source.CodeAnalysis.ConVars +{ + /// + /// Emits Source.Generated.CvarRegistration for every [SourceDll] assembly. The engine uses it in place of the + /// reflection sweep in EngineBuilder.ConVar_Register: one registrar per type that declares ConVar fields/properties + /// or [ConCommand] methods. Private members are reached with [UnsafeAccessor], so the declaring code needs no changes. + /// Anything the generator cannot express exactly is listed for the engine to handle through reflection instead. + /// + [Generator(LanguageNames.CSharp)] + public sealed class ConVarRegistrationGenerator : IIncrementalGenerator + { + const string Commands = "Source.Common.Commands."; + const string G = "global::"; + const string CompilerServices = "global::System.Runtime.CompilerServices."; + const string RegistrarType = "global::System.Action>"; + + public void Initialize(IncrementalGeneratorInitializationContext context) { + context.RegisterSourceOutput(context.CompilationProvider, static (spc, compilation) => Execute(spc, compilation)); + } + + sealed class Symbols + { + public Compilation Compilation = null!; + public INamedTypeSymbol ConVar = null!; + public INamedTypeSymbol? ConVarRef; + public INamedTypeSymbol ConCommandAttribute = null!; + public INamedTypeSymbol? CvarIgnoreAttribute; + public INamedTypeSymbol TokenizedCommand = null!; + public INamedTypeSymbol CommandSource = null!; + public INamedTypeSymbol? EnumerableOfString; + public bool HasUnsafeAccessorType; + public string AssemblyName = ""; + } + + enum MemberKind { Property, Field, Method } + + sealed class Completion + { + public string Name = ""; + public bool Direct; + public string ReturnType = ""; + } + + sealed class Member + { + public MemberKind Kind; + public string Name = ""; + public bool IsStatic; + public bool Inheritable; + public bool Direct; + public string ValueType = ""; + public int Callback; + public string CommandName = ""; + public string HelpText = "null"; + public string Flags = "0"; + public Completion? Completion; + public int Id; + } + + sealed class TypeModel + { + public string MetadataName = ""; + public string FullyQualified = ""; + public readonly List Members = new List(); + } + + static void Execute(SourceProductionContext spc, Compilation compilation) { + if (!compilation.Assembly.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "Source.Common.SourceDllAttribute")) + return; + + INamedTypeSymbol? conVar = compilation.GetTypeByMetadataName(Commands + "ConVar"); + INamedTypeSymbol? conCommandAttribute = compilation.GetTypeByMetadataName(Commands + "ConCommandAttribute"); + INamedTypeSymbol? tokenizedCommand = compilation.GetTypeByMetadataName(Commands + "TokenizedCommand"); + INamedTypeSymbol? commandSource = compilation.GetTypeByMetadataName(Commands + "CommandSource"); + if (conVar == null || conCommandAttribute == null || tokenizedCommand == null || commandSource == null || compilation.GetTypeByMetadataName(Commands + "ICvar") == null) + return; + + Symbols s = new Symbols { + Compilation = compilation, + ConVar = conVar, + ConVarRef = compilation.GetTypeByMetadataName(Commands + "ConVarRef"), + ConCommandAttribute = conCommandAttribute, + CvarIgnoreAttribute = compilation.GetTypeByMetadataName(Commands + "CvarIgnoreAttribute"), + TokenizedCommand = tokenizedCommand, + CommandSource = commandSource, + EnumerableOfString = compilation.GetTypeByMetadataName("System.Collections.Generic.IEnumerable`1")?.Construct(compilation.GetSpecialType(SpecialType.System_String)), + HasUnsafeAccessorType = compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.UnsafeAccessorTypeAttribute") != null, + AssemblyName = compilation.AssemblyName ?? "", + }; + + List models = new List(); + List reflected = new List(); + int nextId = 0; + foreach (INamedTypeSymbol type in AllTypes(compilation.Assembly.GlobalNamespace)) { + spc.CancellationToken.ThrowIfCancellationRequested(); + Analyze(s, type, models, reflected, ref nextId); + } + + spc.AddSource("CvarRegistration.g.cs", SourceText.From(Emit(s, models, reflected), Encoding.UTF8)); + } + + static IEnumerable AllTypes(INamespaceSymbol ns) { + foreach (INamedTypeSymbol type in ns.GetTypeMembers()) + foreach (INamedTypeSymbol t in SelfAndNested(type)) + yield return t; + + foreach (INamespaceSymbol child in ns.GetNamespaceMembers()) + foreach (INamedTypeSymbol t in AllTypes(child)) + yield return t; + } + + static IEnumerable SelfAndNested(INamedTypeSymbol type) { + yield return type; + foreach (INamedTypeSymbol nested in type.GetTypeMembers()) + foreach (INamedTypeSymbol t in SelfAndNested(nested)) + yield return t; + } + + static bool DerivesFrom(ITypeSymbol? type, INamedTypeSymbol target) { + for (ITypeSymbol? t = type; t != null; t = t.BaseType) + if (SymbolEqualityComparer.Default.Equals(t, target)) + return true; + return false; + } + + static bool IsConVarType(Symbols s, ITypeSymbol type) { + if (type is ITypeParameterSymbol tp) + return tp.ConstraintTypes.Any(c => IsConVarType(s, c)); + return DerivesFrom(type, s.ConVar); + } + + static bool HasAttribute(ISymbol symbol, INamedTypeSymbol? attribute) + => attribute != null && symbol.GetAttributes().Any(a => DerivesFrom(a.AttributeClass, attribute)); + + static AttributeData? FindAttribute(ISymbol symbol, INamedTypeSymbol attribute) + => symbol.GetAttributes().FirstOrDefault(a => DerivesFrom(a.AttributeClass, attribute)); + + static string MetadataName(INamedTypeSymbol type) { + if (type.ContainingType != null) + return MetadataName(type.ContainingType) + "+" + type.MetadataName; + if (type.ContainingNamespace == null || type.ContainingNamespace.IsGlobalNamespace) + return type.MetadataName; + return type.ContainingNamespace.ToDisplayString() + "." + type.MetadataName; + } + + static string Fq(ITypeSymbol type) => type.WithNullableAnnotation(NullableAnnotation.NotAnnotated).ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + static string Literal(string value) => SymbolDisplay.FormatLiteral(value, true); + + static bool IsGeneric(INamedTypeSymbol type) { + for (INamedTypeSymbol? t = type; t != null; t = t.ContainingType) + if (t.Arity > 0) + return true; + return false; + } + + static bool IsNameable(Symbols s, ITypeSymbol type) { + switch (type) { + case IArrayTypeSymbol array: + return IsNameable(s, array.ElementType); + case INamedTypeSymbol named: + for (INamedTypeSymbol? t = named; t != null; t = t.ContainingType) + if (t.IsFileLocal || t.IsAnonymousType) + return false; + if (!s.Compilation.IsSymbolAccessibleWithin(named, s.Compilation.Assembly)) + return false; + return named.TypeArguments.All(a => IsNameable(s, a)); + default: + return false; + } + } + + static bool CapturesConVarInPrimaryConstructor(Symbols s, INamedTypeSymbol type) { + foreach (IMethodSymbol ctor in type.InstanceConstructors) { + if (!ctor.DeclaringSyntaxReferences.Any(r => r.GetSyntax() is TypeDeclarationSyntax)) + continue; + if (ctor.Parameters.Any(p => IsConVarType(s, p.Type))) + return true; + } + return false; + } + + static void Analyze(Symbols s, INamedTypeSymbol type, List models, List reflected, ref int nextId) { + if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct && type.TypeKind != TypeKind.Interface) + return; + if (DerivesFrom(type, s.ConVar) || (s.ConVarRef != null && SymbolEqualityComparer.Default.Equals(type, s.ConVarRef))) + return; + + bool nameable = IsNameable(s, type); + bool isStruct = type.TypeKind == TypeKind.Struct; + bool isInterface = type.TypeKind == TypeKind.Interface; + bool candidates = false; + bool reflect = false; + TypeModel model = new TypeModel { MetadataName = MetadataName(type), FullyQualified = Fq(type) }; + + if (type.TypeKind != TypeKind.Interface && CapturesConVarInPrimaryConstructor(s, type)) { + candidates = true; + reflect = true; + } + + foreach (ISymbol symbol in type.GetMembers()) { + switch (symbol) { + case IPropertySymbol property: { + if (property.IsIndexer || !IsConVarType(s, property.Type) || HasAttribute(property, s.CvarIgnoreAttribute)) + break; + if (property.GetMethod == null || property.GetMethod.DeclaredAccessibility != Accessibility.Public) + break; + + candidates = true; + if (isInterface ? property.IsStatic : (property.IsVirtual || property.IsAbstract || property.IsOverride)) + reflect = true; + if (isStruct && !property.IsStatic) + reflect = true; + if (!nameable && !CanUseAccessor(s, type, property.Type)) + reflect = true; + + model.Members.Add(new Member { + Kind = MemberKind.Property, + Name = property.Name, + IsStatic = property.IsStatic, + Inheritable = !property.IsStatic && !isInterface, + Direct = nameable, + ValueType = Fq(property.Type), + Id = nextId++, + }); + break; + } + case IFieldSymbol field: { + if (!IsConVarType(s, field.Type) || HasAttribute(field, s.CvarIgnoreAttribute)) + break; + + candidates = true; + if (isStruct && !field.IsStatic) + reflect = true; + + bool direct = nameable && field.AssociatedSymbol == null && field.CanBeReferencedByName && s.Compilation.IsSymbolAccessibleWithin(field, s.Compilation.Assembly); + if (!direct && !CanUseAccessor(s, type, field.Type)) + reflect = true; + + model.Members.Add(new Member { + Kind = MemberKind.Field, + Name = field.Name, + IsStatic = field.IsStatic, + Inheritable = !field.IsStatic && field.DeclaredAccessibility != Accessibility.Private, + Direct = direct, + ValueType = Fq(field.Type), + Id = nextId++, + }); + break; + } + case IMethodSymbol method: { + AttributeData? attribute = FindAttribute(method, s.ConCommandAttribute); + if (attribute == null) { + for (IMethodSymbol? overridden = method.OverriddenMethod; overridden != null; overridden = overridden.OverriddenMethod) { + if (FindAttribute(overridden, s.ConCommandAttribute) != null) { + candidates = true; + reflect = true; + break; + } + } + break; + } + + candidates = true; + Member? member = AnalyzeCommand(s, type, method, attribute, nameable, isStruct); + if (member == null) + reflect = true; + else { + member.Id = nextId++; + model.Members.Add(member); + } + break; + } + } + } + + if (!candidates) + return; + + if (reflect || IsGeneric(type)) + reflected.Add(model.MetadataName); + else + models.Add(model); + } + + static bool CanUseAccessor(Symbols s, INamedTypeSymbol owner, ITypeSymbol valueType) + => s.HasUnsafeAccessorType && owner.TypeKind == TypeKind.Class && !IsGeneric(owner) && IsNameable(s, valueType); + + static Member? AnalyzeCommand(Symbols s, INamedTypeSymbol type, IMethodSymbol method, AttributeData attribute, bool nameable, bool isStruct) { + if (method.MethodKind != MethodKind.Ordinary || method.IsGenericMethod || !method.ReturnsVoid) + return null; + if (method.IsVirtual || method.IsAbstract || method.IsOverride || type.TypeKind == TypeKind.Interface) + return null; + if (isStruct && !method.IsStatic) + return null; + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, s.ConCommandAttribute) || attribute.ConstructorArguments.Length != 4 || attribute.NamedArguments.Length != 0) + return null; + + int callback; + var parameters = method.Parameters; + if (parameters.Length == 0) + callback = 0; + else if (parameters.Length == 1 && IsCommandParameter(s, parameters[0])) + callback = 1; + else if (parameters.Length == 3 && IsCommandParameter(s, parameters[0]) + && parameters[1].RefKind == RefKind.None && SymbolEqualityComparer.Default.Equals(parameters[1].Type, s.CommandSource) + && parameters[2].RefKind == RefKind.None && parameters[2].Type.SpecialType == SpecialType.System_Int32) + callback = 2; + else + return null; + + TypedConstant name = attribute.ConstructorArguments[0]; + TypedConstant help = attribute.ConstructorArguments[1]; + TypedConstant flags = attribute.ConstructorArguments[2]; + TypedConstant complete = attribute.ConstructorArguments[3]; + if (name.Kind == TypedConstantKind.Error || help.Kind == TypedConstantKind.Error || flags.Kind == TypedConstantKind.Error || complete.Kind == TypedConstantKind.Error || flags.Type == null) + return null; + + bool direct = nameable && s.Compilation.IsSymbolAccessibleWithin(method, s.Compilation.Assembly); + if (!direct && !CanUseAccessor(s, type, s.TokenizedCommand)) + return null; + + Member member = new Member { + Kind = MemberKind.Method, + Name = method.Name, + IsStatic = method.IsStatic, + Inheritable = !method.IsStatic && method.DeclaredAccessibility != Accessibility.Private, + Direct = direct, + Callback = callback, + CommandName = Literal(name.Value as string ?? method.Name), + HelpText = help.Value is string helpText ? Literal(helpText) : "null", + Flags = "unchecked((" + Fq(flags.Type) + ")(" + (flags.Value is ulong u ? u.ToString() + "UL" : Convert.ToInt64(flags.Value ?? 0).ToString() + "L") + "))", + }; + + if (complete.Value is string completeName) { + IMethodSymbol[] found = type.GetMembers(completeName).OfType().ToArray(); + if (found.Length != 1) + return null; + + IMethodSymbol completion = found[0]; + if (completion.MethodKind != MethodKind.Ordinary || completion.IsGenericMethod) + return null; + + bool bindable = completion.IsStatic == method.IsStatic && s.EnumerableOfString != null && !completion.ReturnsVoid + && completion.Parameters.Length == 1 && completion.Parameters[0].RefKind == RefKind.None; + if (bindable && completion.Parameters[0].Type.SpecialType != SpecialType.System_String) + return null; + + if (bindable) { + Conversion conversion = ((CSharpCompilation)s.Compilation).ClassifyConversion(completion.ReturnType, s.EnumerableOfString!); + if (!conversion.IsIdentity && !(conversion.IsImplicit && conversion.IsReference)) + bindable = false; + } + + if (bindable) { + bool completionDirect = nameable && s.Compilation.IsSymbolAccessibleWithin(completion, s.Compilation.Assembly); + if (!completionDirect && !CanUseAccessor(s, type, completion.ReturnType)) + return null; + + member.Completion = new Completion { Name = completion.Name, Direct = completionDirect, ReturnType = Fq(completion.ReturnType) }; + } + } + + return member; + } + + static bool IsCommandParameter(Symbols s, IParameterSymbol parameter) + => parameter.RefKind == RefKind.In && SymbolEqualityComparer.Default.Equals(parameter.Type, s.TokenizedCommand); + + static string Emit(Symbols s, List models, List reflected) { + StringBuilder sb = new StringBuilder(); + StringBuilder accessors = new StringBuilder(); + sb.Append("// \n#nullable disable\n#pragma warning disable\n\nnamespace Source.Generated\n{\n"); + sb.Append("\tinternal static class CvarRegistration\n\t{\n"); + sb.Append("\t\tpublic static void Populate(global::System.Collections.Generic.Dictionary declared, global::System.Collections.Generic.Dictionary inherited, global::System.Collections.Generic.List reflected) {\n"); + for (int i = 0; i < models.Count; i++) { + sb.Append("\t\t\tdeclared.Add(").Append(Literal(models[i].MetadataName)).Append(", D").Append(i).Append(");\n"); + if (models[i].Members.Any(m => m.Inheritable)) + sb.Append("\t\t\tinherited.Add(").Append(Literal(models[i].MetadataName)).Append(", I").Append(i).Append(");\n"); + } + foreach (string name in reflected) + sb.Append("\t\t\treflected.Add(").Append(Literal(name)).Append(");\n"); + sb.Append("\t\t}\n\n"); + + sb.Append("\t\tstatic object Inst(global::System.Func resolve, global::System.Type declaring, global::System.Type type, bool concommand, string name) {\n"); + sb.Append("\t\t\tobject instance = resolve(type, concommand, name);\n"); + sb.Append("\t\t\tif (!declaring.IsInstanceOfType(instance))\n"); + sb.Append("\t\t\t\tthrow new global::System.InvalidCastException($\"The instance resolved for '{type}' is not a '{declaring}', which declares '{name}'.\");\n"); + sb.Append("\t\t\treturn instance;\n\t\t}\n\n"); + sb.Append("\t\tstatic void Reg(global::Source.Common.Commands.ICvar cvar, global::Source.Common.Commands.ConVar cv, string name) {\n"); + sb.Append("\t\t\tif (cv == null)\n\t\t\t\treturn;\n"); + sb.Append("\t\t\tif (cv.GetName() == null)\n\t\t\t\tcv.SetName(name);\n"); + sb.Append("\t\t\tcvar.RegisterConCommand(cv);\n\t\t}\n"); + + for (int i = 0; i < models.Count; i++) { + EmitRegistrar(s, sb, accessors, models[i], "D" + i, inheritedOnly: false); + if (models[i].Members.Any(m => m.Inheritable)) + EmitRegistrar(s, sb, accessors, models[i], "I" + i, inheritedOnly: true); + } + + sb.Append(accessors); + sb.Append("\t}\n}\n"); + return sb.ToString(); + } + + static void EmitRegistrar(Symbols s, StringBuilder sb, StringBuilder accessors, TypeModel model, string name, bool inheritedOnly) { + sb.Append("\n\t\t// ").Append(model.MetadataName).Append(inheritedOnly ? " (members inherited by derived types)" : "").Append('\n'); + sb.Append("\t\tstatic void ").Append(name).Append("(int phase, global::System.Type declaring, global::System.Type type, global::Source.Common.Commands.ICvar cvar, global::System.Func resolve) {\n"); + sb.Append("\t\t\tswitch (phase) {\n"); + foreach (MemberKind kind in new[] { MemberKind.Property, MemberKind.Field, MemberKind.Method }) { + // The runtime lists a type's instance fields before its static fields; keep that order so that + // duplicate names resolve to the same parent ConVar as they do through reflection. + Member[] members = model.Members.Where(m => m.Kind == kind && (!inheritedOnly || m.Inheritable)) + .OrderBy(m => kind == MemberKind.Field && m.IsStatic ? 1 : 0).ToArray(); + if (members.Length == 0) + continue; + + sb.Append("\t\t\t\tcase ").Append((int)kind).Append(":\n"); + foreach (Member member in members) { + if (!inheritedOnly) + EmitAccessors(s, accessors, model, member); + if (kind == MemberKind.Method) + EmitCommand(sb, model, member); + else + EmitConVar(sb, model, member); + } + sb.Append("\t\t\t\t\tbreak;\n"); + } + sb.Append("\t\t\t}\n\t\t}\n"); + } + + static string Id(string name) => "@" + name; + + static void EmitConVar(StringBuilder sb, TypeModel model, Member member) { + string nameLiteral = Literal(member.Name); + string value; + if (member.Direct) + value = (member.IsStatic ? model.FullyQualified : "((" + model.FullyQualified + ")i)") + "." + Id(member.Name); + else + value = "A" + member.Id + "(" + (member.IsStatic ? "null" : "i") + ")"; + + if (member.IsStatic) + sb.Append("\t\t\t\t\tReg(cvar, ").Append(value).Append(", ").Append(nameLiteral).Append(");\n"); + else + sb.Append("\t\t\t\t\t{ object i = Inst(resolve, declaring, type, false, ").Append(nameLiteral).Append("); Reg(cvar, ").Append(value).Append(", ").Append(nameLiteral).Append("); }\n"); + } + + static void EmitCommand(StringBuilder sb, TypeModel model, Member member) { + string target = member.IsStatic ? model.FullyQualified : "((" + model.FullyQualified + ")i)"; + string arg = member.IsStatic ? "null" : "i"; + string callbackType = G + Commands + (member.Callback == 0 ? "FnCommandCallbackVoid" : member.Callback == 1 ? "FnCommandCallback" : "FnCommandCallbackSourced"); + string tokenized = G + Commands + "TokenizedCommand"; + string source = G + Commands + "CommandSource"; + + string callback; + if (member.Direct) + callback = target + "." + Id(member.Name); + else if (member.Callback == 0) + callback = "() => A" + member.Id + "(" + arg + ")"; + else if (member.Callback == 1) + callback = "(in " + tokenized + " a) => A" + member.Id + "(" + arg + ", in a)"; + else + callback = "(in " + tokenized + " a, " + source + " b, int c) => A" + member.Id + "(" + arg + ", in a, b, c)"; + + string completion = "null"; + if (member.Completion != null) { + completion = "(" + G + Commands + "FnCommandCompletionCallback)(" + (member.Completion.Direct + ? target + "." + Id(member.Completion.Name) + : "(string a) => C" + member.Id + "(" + arg + ", a)") + ")"; + } + + sb.Append("\t\t\t\t\t{ "); + if (!member.IsStatic) + sb.Append("object i = Inst(resolve, declaring, type, true, ").Append(Literal(member.Name)).Append("); "); + sb.Append("cvar.RegisterConCommand(new ").Append(G).Append(Commands).Append("ConCommand(").Append(member.CommandName) + .Append(", (").Append(callbackType).Append(")(").Append(callback).Append("), ") + .Append(member.HelpText).Append(", ").Append(member.Flags).Append(", ").Append(completion).Append(")); }\n"); + } + + static void EmitAccessors(Symbols s, StringBuilder accessors, TypeModel model, Member member) { + string owner = "[" + CompilerServices + "UnsafeAccessorType(" + Literal(model.MetadataName + ", " + s.AssemblyName) + ")] object _"; + string tokenized = G + Commands + "TokenizedCommand"; + + if (!member.Direct) { + accessors.Append('\n'); + switch (member.Kind) { + case MemberKind.Field: + AppendAccessorAttribute(accessors, member.IsStatic ? "StaticField" : "Field", member.Name); + accessors.Append("\t\tstatic extern ref ").Append(member.ValueType).Append(" A").Append(member.Id).Append('(').Append(owner).Append(");\n"); + break; + case MemberKind.Property: + AppendAccessorAttribute(accessors, member.IsStatic ? "StaticMethod" : "Method", "get_" + member.Name); + accessors.Append("\t\tstatic extern ").Append(member.ValueType).Append(" A").Append(member.Id).Append('(').Append(owner).Append(");\n"); + break; + case MemberKind.Method: + AppendAccessorAttribute(accessors, member.IsStatic ? "StaticMethod" : "Method", member.Name); + accessors.Append("\t\tstatic extern void A").Append(member.Id).Append('(').Append(owner); + if (member.Callback >= 1) + accessors.Append(", in ").Append(tokenized).Append(" a"); + if (member.Callback == 2) + accessors.Append(", ").Append(G).Append(Commands).Append("CommandSource b, int c"); + accessors.Append(");\n"); + break; + } + } + + if (member.Completion != null && !member.Completion.Direct) { + accessors.Append('\n'); + AppendAccessorAttribute(accessors, member.IsStatic ? "StaticMethod" : "Method", member.Completion.Name); + accessors.Append("\t\tstatic extern ").Append(member.Completion.ReturnType).Append(" C").Append(member.Id).Append('(').Append(owner).Append(", string a);\n"); + } + } + + static void AppendAccessorAttribute(StringBuilder accessors, string kind, string name) { + accessors.Append("\t\t[").Append(CompilerServices).Append("UnsafeAccessor(").Append(CompilerServices).Append("UnsafeAccessorKind.").Append(kind) + .Append(", Name = ").Append(Literal(name)).Append(")]\n"); + } + } +} diff --git a/Source.CodeAnalysis.ConVars/Source.CodeAnalysis.ConVars.csproj b/Source.CodeAnalysis.ConVars/Source.CodeAnalysis.ConVars.csproj new file mode 100644 index 000000000..5fe2175b2 --- /dev/null +++ b/Source.CodeAnalysis.ConVars/Source.CodeAnalysis.ConVars.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + false + true + true + latest + enable + false + Debug;Release;Debug (Dedicated);Release (Dedicated) + + + + + + + + diff --git a/Source.Common.Tests/KeyValuesTests.cs b/Source.Common.Tests/KeyValuesTests.cs new file mode 100644 index 000000000..ba54ca3e7 --- /dev/null +++ b/Source.Common.Tests/KeyValuesTests.cs @@ -0,0 +1,590 @@ +using Source.Common.Formats.Keyvalues; + +using System.Text; + +namespace Source.Common.Tests; + +public class KeyValuesTests +{ + static KeyValues Parse(string text, bool escapeSequences = false) { + KeyValues kv = new(); + kv.UsesEscapeSequences(escapeSequences); + Assert.True(kv.LoadFromBuffer("test", text)); + return kv; + } + + static KeyValues Value(string rawValue, bool escapeSequences = false) + => Parse("\"Root\"\n{\n\t\"v\"\t" + rawValue + "\n}\n", escapeSequences).FindKey("v")!; + + static string[] ChildNames(KeyValues kv) => [.. kv.Select(x => x.Name)]; + + [Fact] + public void SkipWhitespaceMatchesCharIsWhiteSpace() { + for (int c = char.MinValue; c <= char.MaxValue; c++) { + KeyValues.KeyValuesReader reader = new((char)c + "x"); + + Assert.Equal(char.IsWhiteSpace((char)c), KeyValues.SkipWhitespace(reader)); + Assert.Equal(char.IsWhiteSpace((char)c) ? 'x' : c, reader.Peek()); + } + } + + [Fact] + public void QuotedRootWithPairs() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n\t\"b\"\t\"two\"\n}\n"); + + Assert.Equal("Root", kv.Name); + Assert.Equal(KeyValues.Types.None, kv.Type); + Assert.Equal(2, kv.Count); + Assert.Equal(["a", "b"], ChildNames(kv)); + Assert.Equal(1, kv.GetInt("a")); + Assert.Equal("two", kv.GetString("b").ToString()); + } + + [Fact] + public void UnquotedTokens() { + KeyValues kv = Parse("Root\n{\n\tkey value\n\tnum 42\n}\n"); + + Assert.Equal("Root", kv.Name); + Assert.Equal(["key", "num"], ChildNames(kv)); + Assert.Equal("value", kv.GetString("key").ToString()); + Assert.Equal(42, kv.GetInt("num")); + } + + [Fact] + public void MixedQuotedAndUnquotedTokens() { + KeyValues kv = Parse("\"Root\"\n{\n\tkey \"quoted value\"\n\t\"quoted key\" value\n}\n"); + + Assert.Equal("quoted value", kv.GetString("key").ToString()); + Assert.Equal("value", kv.GetString("quoted key").ToString()); + } + + [Fact] + public void NestedSectionsKeepOrder() { + KeyValues kv = Parse(""" + "Root" + { + "first" "1" + "Section" + { + "inner" "x" + "Deeper" + { + "leaf" "y" + } + } + "last" "2" + } + """); + + Assert.Equal(["first", "Section", "last"], ChildNames(kv)); + + KeyValues section = kv.FindKey("Section")!; + Assert.Equal(KeyValues.Types.None, section.Type); + Assert.Equal(["inner", "Deeper"], ChildNames(section)); + Assert.Equal("x", section.GetString("inner").ToString()); + Assert.Equal("y", section.FindKey("Deeper")!.GetString("leaf").ToString()); + Assert.Equal(2, kv.GetInt("last")); + } + + [Fact] + public void EmptySection() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"Empty\"\n\t{\n\t}\n\t\"after\"\t\"1\"\n}\n"); + + KeyValues empty = kv.FindKey("Empty")!; + Assert.Equal(KeyValues.Types.None, empty.Type); + Assert.Equal(0, empty.Count); + Assert.Equal(1, kv.GetInt("after")); + } + + [Fact] + public void CompactSingleLine() { + KeyValues kv = Parse("\"Root\"{\"a\" \"1\" \"Sub\"{\"b\" \"2\"}\"c\" \"3\"}"); + + Assert.Equal(["a", "Sub", "c"], ChildNames(kv)); + Assert.Equal(2, kv.FindKey("Sub")!.GetInt("b")); + Assert.Equal(3, kv.GetInt("c")); + } + + [Fact] + public void NoTrailingNewline() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n}"); + + Assert.Equal(1, kv.Count); + Assert.Equal(1, kv.GetInt("a")); + } + + [Fact] + public void WindowsLineEndingsAndTabs() { + KeyValues kv = Parse("\"Root\"\r\n{\r\n\t\"a\"\t\t\"1\"\r\n\t\"b\"\t\t\"two words\"\r\n}\r\n"); + + Assert.Equal(["a", "b"], ChildNames(kv)); + Assert.Equal("two words", kv.GetString("b").ToString()); + } + + [Fact] + public void CommentsAreSkipped() { + KeyValues kv = Parse(""" + // leading comment + "Root" // after the root name + { + // a full line comment + "a" "1" // trailing comment + //"commented" "out" + "b" "2" + } + // comment at the end with no newline + """); + + Assert.Equal("Root", kv.Name); + Assert.Equal(["a", "b"], ChildNames(kv)); + Assert.Equal(1, kv.GetInt("a")); + Assert.Equal(2, kv.GetInt("b")); + } + + [Fact] + public void QuotedValueKeepsSpecialCharacters() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"k\"\t\"a b { } // not a comment [$X360]\"\n\t\"after\"\t\"1\"\n}\n"); + + Assert.Equal("a b { } // not a comment [$X360]", kv.GetString("k").ToString()); + Assert.Equal(1, kv.GetInt("after")); + } + + [Fact] + public void UnquotedValueMayContainSlash() { + KeyValues kv = Parse("\"Root\"\n{\n\tpath sound/weapons/shot.wav\n\tabs /root/file\n}\n"); + + Assert.Equal("sound/weapons/shot.wav", kv.GetString("path").ToString()); + Assert.Equal("/root/file", kv.GetString("abs").ToString()); + } + + [Fact] + public void DuplicateKeysAreKeptInOrder() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"wave\"\t\"a.wav\"\n\t\"wave\"\t\"b.wav\"\n\t\"wave\"\t\"c.wav\"\n}\n"); + + Assert.Equal(3, kv.Count); + Assert.Equal(["a.wav", "b.wav", "c.wav"], kv.Select(x => x.GetString().ToString()).ToArray()); + Assert.Equal("a.wav", kv.GetString("wave").ToString()); + } + + [Fact] + public void FindKeyIsCaseInsensitive() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"MixedCase\"\t\"1\"\n}\n"); + + Assert.NotNull(kv.FindKey("mixedcase")); + Assert.NotNull(kv.FindKey("MIXEDCASE")); + Assert.Equal("MixedCase", kv.FindKey("mixedcase")!.Name); + Assert.Null(kv.FindKey("missing")); + } + + [Theory] + [InlineData("\"5\"", 5)] + [InlineData("\"-12\"", -12)] + [InlineData("\"+7\"", 7)] + [InlineData("\"0\"", 0)] + [InlineData("42", 42)] + [InlineData("\"2147483647\"", int.MaxValue)] + public void IntegerValues(string raw, int expected) { + KeyValues v = Value(raw); + + Assert.Equal(KeyValues.Types.Int, v.Type); + Assert.Equal(expected, Assert.IsType(v.Value)); + } + + [Theory] + [InlineData("\"1.5\"", 1.5)] + [InlineData("\"-0.25\"", -0.25)] + [InlineData("\".5\"", 0.5)] + [InlineData("\"1e3\"", 1000.0)] + [InlineData("0.75", 0.75)] + public void FloatValues(string raw, double expected) { + KeyValues v = Value(raw); + + Assert.Equal(KeyValues.Types.Double, v.Type); + Assert.Equal(expected, Assert.IsType(v.Value)); + } + + [Fact] + public void FloatValuesAreSinglePrecision() { + KeyValues v = Value("\"1.1\""); + + Assert.Equal((double)1.1f, Assert.IsType(v.Value)); + } + + [Theory] + [InlineData("\"abc\"", "abc")] + [InlineData("\"12abc\"", "12abc")] + [InlineData("\"1.5.2\"", "1.5.2")] + [InlineData("\"\"", "")] + [InlineData("\"99999999999\"", "99999999999")] + [InlineData("\"0x10\"", "0x10")] + [InlineData("\"255 128 0 255\"", "255 128 0 255")] + [InlineData("word", "word")] + public void StringValues(string raw, string expected) { + KeyValues v = Value(raw); + + Assert.Equal(KeyValues.Types.String, v.Type); + Assert.Equal(expected, Assert.IsType(v.Value)); + } + + [Theory] + [InlineData("\"0x00000000000000FF\"", 0xFFul)] + [InlineData("\"0x0123456789abcdef\"", 0x0123456789ABCDEFul)] + [InlineData("\"0xFFFFFFFFFFFFFFFF\"", ulong.MaxValue)] + public void Uint64Values(string raw, ulong expected) { + KeyValues v = Value(raw); + + Assert.Equal(KeyValues.Types.Uint64, v.Type); + Assert.Equal(expected, Assert.IsType(v.Value)); + } + + [Fact] + public void BackslashesAreLiteralWithoutEscapeSequences() { + KeyValues v = Value("\"C:\\dir\\new\"", escapeSequences: false); + + Assert.Equal("C:\\dir\\new", v.GetString().ToString()); + } + + [Fact] + public void EscapeSequencesAreTranslated() { + KeyValues v = Value("\"line1\\nline2 q:\\\"x\\\"\"", escapeSequences: true); + + Assert.Equal("line1\nline2 q:\"x\"", v.GetString().ToString()); + } + + [Fact] + public void ConditionalsOnValues() { + KeyValues kv = Parse(""" + "Root" + { + "pc" "1" [$WIN32] + "notpc" "1" [!$WIN32] + "x360" "1" [$X360] + "notx360" "1" [!$X360] + "unknown" "1" [$SOMETHINGELSE] + "notunknown" "1" [!$SOMETHINGELSE] + "plain" "1" + } + """); + + Assert.Equal(["pc", "notx360", "notunknown", "plain"], ChildNames(kv)); + } + + [Fact] + public void ConditionalsOnSections() { + KeyValues kv = Parse(""" + "Root" + { + "Console" [$X360] + { + "a" "1" + } + "Desktop" [$WIN32] + { + "b" "2" + } + "after" "3" + } + """); + + Assert.Equal(["Desktop", "after"], ChildNames(kv)); + Assert.Equal(2, kv.FindKey("Desktop")!.GetInt("b")); + Assert.Equal(3, kv.GetInt("after")); + } + + [Fact] + public void PlatformConditionalsMatchTheBuildPlatform() { + KeyValues kv = Parse(""" + "Root" + { + "windows" "1" [$WINDOWS] + "linux" "1" [$LINUX] + "osx" "1" [$OSX] + "posix" "1" [$POSIX] + } + """); + + Assert.Equal(OperatingSystem.IsWindows(), kv.FindKey("windows") != null); + Assert.Equal(OperatingSystem.IsLinux(), kv.FindKey("linux") != null); + Assert.Equal(OperatingSystem.IsMacOS(), kv.FindKey("osx") != null); + Assert.Equal(OperatingSystem.IsLinux(), kv.FindKey("posix") != null); + } + + [Fact] + public void MultipleRootsBecomePeers() { + KeyValues kv = Parse("\"A\"\n{\n\t\"x\"\t\"1\"\n}\n\"B\"\n{\n\t\"y\"\t\"2\"\n}\n"); + + Assert.Equal("A", kv.Name); + Assert.Equal(1, kv.GetInt("x")); + + KeyValues? b = kv.GetNextKey(); + Assert.NotNull(b); + Assert.Equal("B", b.Name); + Assert.Equal(2, b.GetInt("y")); + Assert.Null(b.GetNextKey()); + } + + [Theory] + [InlineData("")] + [InlineData(" \r\n\t\n")] + [InlineData("// only a comment\n")] + public void NothingToParseFails(string text) { + KeyValues kv = new(); + + Assert.False(kv.LoadFromBuffer("test", text)); + } + + [Fact] + public void NullStreamFails() { + Assert.False(new KeyValues().LoadFromStream(null)); + } + + [Fact] + public void StreamWithUtf8ByteOrderMark() { + byte[] bytes = [.. Encoding.UTF8.GetPreamble(), .. Encoding.UTF8.GetBytes("\"Root\"\n{\n\t\"name\"\t\"caf\u00e9\"\n}\n")]; + KeyValues kv = new(); + + Assert.True(kv.LoadFromStream(new MemoryStream(bytes))); + Assert.Equal("Root", kv.Name); + Assert.Equal("caf\u00e9", kv.GetString("name").ToString()); + } + + [Fact] + public void SoundScriptShapedFile() { + KeyValues kv = Parse(""" + // Channels + // CHAN_AUTO = 0, + + "Weapon_Pistol.Single" + { + "channel" "CHAN_WEAPON" + "volume" "0.55" + "soundlevel" "SNDLVL_GUNFIRE" + "pitch" "98,102" + + "rndwave" + { + "wave" ")weapons/pistol/pistol_fire2.wav" + "wave" ")weapons/pistol/pistol_fire3.wav" + } + } + + "Weapon_Pistol.Empty" + { + "channel" "CHAN_ITEM" + "volume" "0.8" + "soundlevel" "SNDLVL_NORM" + "pitch" "PITCH_NORM" + "wave" "weapons/pistol/pistol_empty.wav" + } + """); + + Assert.Equal("Weapon_Pistol.Single", kv.Name); + Assert.Equal("CHAN_WEAPON", kv.GetString("channel").ToString()); + Assert.Equal(0.55f, kv.GetFloat("volume")); + Assert.Equal("98,102", kv.GetString("pitch").ToString()); + + KeyValues rndwave = kv.FindKey("rndwave")!; + Assert.Equal([")weapons/pistol/pistol_fire2.wav", ")weapons/pistol/pistol_fire3.wav"], rndwave.Select(x => x.GetString().ToString()).ToArray()); + + KeyValues empty = kv.GetNextKey()!; + Assert.Equal("Weapon_Pistol.Empty", empty.Name); + Assert.Equal("weapons/pistol/pistol_empty.wav", empty.GetString("wave").ToString()); + Assert.Null(empty.GetNextKey()); + } + + [Fact] + public void MissingKeysReturnDefaults() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n}\n"); + + Assert.Equal("fallback", kv.GetString("missing", "fallback").ToString()); + Assert.True(kv.GetString("missing").IsEmpty); + Assert.Equal(7, kv.GetInt("missing", 7)); + Assert.Equal(0, kv.GetInt("missing")); + Assert.Equal(1.5f, kv.GetFloat("missing", 1.5f)); + Assert.Equal(2.5, kv.GetDouble("missing", 2.5)); + Assert.True(kv.GetBool("missing", true)); + Assert.False(kv.GetBool("missing", false)); + Assert.Equal(9ul, kv.GetUint64("missing", 9ul)); + Assert.Equal(new Color(1, 2, 3, 4), kv.GetColor("missing", new Color(1, 2, 3, 4))); + Assert.Null(kv.GetPtr("missing")); + } + + [Fact] + public void NumericConversionsByKey() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"i\"\t\"5\"\n\t\"f\"\t\"2.75\"\n\t\"s\"\t\"text\"\n\t\"zero\"\t\"0\"\n}\n"); + + Assert.Equal(5, kv.GetInt("i")); + Assert.Equal(5f, kv.GetFloat("i")); + Assert.Equal(5.0, kv.GetDouble("i")); + Assert.True(kv.GetBool("i", false)); + + Assert.Equal(2, kv.GetInt("f")); + Assert.Equal(2.75f, kv.GetFloat("f")); + Assert.Equal(2.75, kv.GetDouble("f")); + + Assert.Equal(3, kv.GetInt("s", 3)); + Assert.Equal(3f, kv.GetFloat("s", 3f)); + Assert.False(kv.GetBool("zero", true)); + } + + [Fact] + public void GetStringFormatsNumbers() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"i\"\t\"5\"\n\t\"f\"\t\"1.5\"\n}\n"); + + Assert.Equal("5", kv.GetString("i").ToString()); + Assert.Equal("1.500000", kv.GetString("f").ToString()); + } + + [Fact] + public void GetColorParsesComponents() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"c\"\t\"255 128 0 64\"\n}\n"); + + Assert.Equal(new Color(255, 128, 0, 64), kv.GetColor("c")); + } + + [Fact] + public void SettersCreateAndOverwriteKeys() { + KeyValues kv = new("Root"); + + kv.SetString("s", "one"); + kv.SetInt("i", 3); + kv.SetFloat("f", 0.5f); + kv.SetUint64("u", 0x1_0000_0000ul); + kv.SetColor("c", new Color(1, 2, 3, 4)); + kv.SetString("s", "two"); + kv.SetInt("I", 4); + + Assert.Equal(["s", "i", "f", "u", "c"], ChildNames(kv)); + Assert.Equal("two", kv.GetString("s").ToString()); + Assert.Equal(4, kv.GetInt("i")); + Assert.Equal(0.5f, kv.GetFloat("f")); + Assert.Equal(0x1_0000_0000ul, kv.GetUint64("u")); + Assert.Equal(new Color(1, 2, 3, 4), kv.GetColor("c")); + } + + [Fact] + public void FindKeyCanCreate() { + KeyValues kv = new("Root"); + + Assert.Null(kv.FindKey("new")); + KeyValues created = kv.FindKey("new", create: true)!; + + Assert.Equal("new", created.Name); + Assert.Same(created, kv.FindKey("NEW")); + Assert.Equal(1, kv.Count); + } + + [Fact] + public void SubKeyIterationHelpers() { + KeyValues kv = Parse(""" + "Root" + { + "v1" "1" + "S1" + { + "a" "1" + } + "v2" "2" + "S2" + { + "b" "2" + } + } + """); + + List all = []; + for (KeyValues? k = kv.GetFirstSubKey(); k != null; k = k.GetNextKey()) + all.Add(k.Name); + Assert.Equal(["v1", "S1", "v2", "S2"], all); + + List sections = []; + for (KeyValues? k = kv.GetFirstTrueSubKey(); k != null; k = k.GetNextTrueSubKey()) + sections.Add(k.Name); + Assert.Equal(["S1", "S2"], sections); + + List values = []; + for (KeyValues? k = kv.GetFirstValue(); k != null; k = k.GetNextValue()) + values.Add(k.Name); + Assert.Equal(["v1", "v2"], values); + } + + [Fact] + public void IsEmptyReportsMissingAndEmptySections() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"Empty\"\n\t{\n\t}\n\t\"Full\"\n\t{\n\t\t\"a\"\t\"1\"\n\t}\n\t\"value\"\t\"1\"\n}\n"); + + Assert.True(kv.IsEmpty("missing")); + Assert.True(kv.IsEmpty("Empty")); + Assert.False(kv.IsEmpty("Full")); + Assert.False(kv.IsEmpty("value")); + } + + [Fact] + public void MakeCopyIsDeep() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n\t\"Sub\"\n\t{\n\t\t\"b\"\t\"text\"\n\t}\n}\n"); + + KeyValues copy = kv.MakeCopy(); + copy.SetInt("a", 99); + copy.FindKey("Sub")!.SetString("b", "changed"); + copy.SetString("added", "x"); + + Assert.Equal("Root", copy.Name); + Assert.Equal(1, kv.GetInt("a")); + Assert.Equal("text", kv.FindKey("Sub")!.GetString("b").ToString()); + Assert.Null(kv.FindKey("added")); + Assert.Equal(99, copy.GetInt("a")); + Assert.Equal("changed", copy.FindKey("Sub")!.GetString("b").ToString()); + } + + [Fact] + public void RemoveSubKey() { + KeyValues kv = Parse("\"Root\"\n{\n\t\"a\"\t\"1\"\n\t\"b\"\t\"2\"\n\t\"c\"\t\"3\"\n}\n"); + + kv.RemoveSubKey(kv.FindKey("b")); + Assert.Equal(["a", "c"], ChildNames(kv)); + + kv.RemoveSubKey(kv.FindKey("a")); + Assert.Equal(["c"], ChildNames(kv)); + + kv.RemoveSubKey(null); + Assert.Equal(["c"], ChildNames(kv)); + } + + [Fact] + public void CreateNewKeyUsesNextNumber() { + KeyValues kv = new("Root"); + + Assert.Equal("1", kv.CreateNewKey().Name); + Assert.Equal("2", kv.CreateNewKey().Name); + kv.CreateKey("10"); + Assert.Equal("11", kv.CreateNewKey().Name); + Assert.Equal(4, kv.Count); + } + + [Fact] + public void ChainedKeyValuesAreSearchedAfterOwnKeys() { + KeyValues baseKv = Parse("\"Base\"\n{\n\t\"shared\"\t\"base\"\n\t\"onlybase\"\t\"1\"\n}\n"); + KeyValues kv = Parse("\"Derived\"\n{\n\t\"shared\"\t\"derived\"\n}\n"); + kv.ChainKeyValue(baseKv); + + Assert.Equal("derived", kv.GetString("shared").ToString()); + Assert.Equal(1, kv.GetInt("onlybase")); + Assert.Null(kv.FindKey("nowhere")); + } + + [Fact] + public void DirectValueSetters() { + KeyValues kv = new("k"); + + kv.SetStringValue("text"); + Assert.Equal(KeyValues.Types.String, kv.Type); + Assert.Equal("text", kv.GetString().ToString()); + + kv.SetInt(12); + Assert.Equal(KeyValues.Types.Int, kv.Type); + Assert.Equal(12, kv.GetInt()); + Assert.True(kv.GetBool()); + + kv.SetName("renamed"); + Assert.Equal("renamed", kv.Name); + } +} diff --git a/Source.Common/Engine/IModelRender.cs b/Source.Common/Engine/IModelRender.cs index b57acfdd4..d660efcd6 100644 --- a/Source.Common/Engine/IModelRender.cs +++ b/Source.Common/Engine/IModelRender.cs @@ -51,12 +51,23 @@ public struct ModelRenderInfo public ModelInstanceHandle_t Instance; } +public struct StaticPropRenderInfo +{ + public Matrix3x4 ModelToWorld; + public Model? Model; + public IClientRenderable? Renderable; + public Vector3 LightingOrigin; + public short Skin; + public ModelInstanceHandle_t Instance; +} + public interface IModelRender { ModelInstanceHandle_t CreateInstance(IClientRenderable renderable, LightCacheHandle_t? cache = null); void DestroyInstance(ModelInstanceHandle_t modelInstance); int DrawModel(StudioFlags flags, IClientRenderable? renderable, ModelInstanceHandle_t instance, int entityIndex, Model? model, in Vector3 origin, in QAngle angles, int skin, int body, int hitboxset, Matrix3x4? modelToWorld = null, Matrix3x4? lightingOffset = null); int DrawModelEx(ref ModelRenderInfo info); + int DrawStaticPropArrayFast(Span props, int count, bool shadowDepth); void SetStaticLighting(ModelInstanceHandle_t handle, LightCacheHandle_t? cache); void SetViewTarget(StudioHdr studioHdr, int bodyIndex, in Vector3 target); void DrawModelExecute(ref DrawModelState state, ref ModelRenderInfo info, Span boneToWorldArray); diff --git a/Source.Common/Formats/Keyvalues/KeyValues.cs b/Source.Common/Formats/Keyvalues/KeyValues.cs index bcee945be..cc1282ac6 100644 --- a/Source.Common/Formats/Keyvalues/KeyValues.cs +++ b/Source.Common/Formats/Keyvalues/KeyValues.cs @@ -102,26 +102,53 @@ public bool LoadFromStream(Stream? stream) { // Clear(); if (stream == null) return false; - using KeyValuesReader reader = new KeyValuesReader(stream); - return LoadFromBuffer(reader); + // Size the buffer to the stream when we can, so the text is decoded in one piece. + int bufferSize = stream.CanSeek ? (int)Math.Clamp(stream.Length - stream.Position, 1024, 1 << 20) : 1024; + using StreamReader streamReader = new(stream, System.Text.Encoding.UTF8, true, bufferSize); + return LoadFromBuffer(new KeyValuesReader(streamReader.ReadToEnd())); } - public sealed class KeyValuesReader(Stream stream) : StreamReader(stream) + public sealed class KeyValuesReader(string text) { - int Pushback = -1; + readonly string text = text; + int position; - public void Unread(char c) => Pushback = c; + public ReadOnlySpan Remaining => text.AsSpan(position); - public override int Peek() => Pushback != -1 ? Pushback : base.Peek(); + public void Advance(int count) => position += count; - public override int Read() { - if (Pushback != -1) { - int c = Pushback; - Pushback = -1; - return c; + public string ReadString(int count) { + string str = text.Substring(position, count); + position += count; + return str; + } + + public void Unread(char c) { + Debug.Assert(position > 0 && text[position - 1] == c, "can only unread the character that was just read"); + position--; + } + + public unsafe bool SkipWhitespace() { + int start = position; + fixed (char* chars = text) { + int i = position, length = text.Length; + while (i < length) { + char c = chars[i]; + if (c != ' ' && (c < '\t' || c > '\r') && (c < 0x80 || !char.IsWhiteSpace(c))) + break; + i++; + } + position = i; } - return base.Read(); + + return position != start; } + + public int Position => position; + + public int Peek() => position < text.Length ? text[position] : -1; + + public int Read() => position < text.Length ? text[position++] : -1; } public bool WriteToStream(Stream? stream) { @@ -199,18 +226,7 @@ private bool LoadFromBuffer(KeyValuesReader reader) { // Returns true if we did anything at all to skip whitespace. public static bool SkipWhitespace(KeyValuesReader reader) { - bool didAnything = false; - while (true) { - int c = reader.Peek(); - if (c == -1) - break; - if (!char.IsWhiteSpace((char)c)) - break; - reader.Read(); - didAnything = true; - } - - return didAnything; + return reader.SkipWhitespace(); } public override string ToString() { @@ -224,8 +240,12 @@ private bool SkipUntilParseableTextOrEOF(KeyValuesReader reader) { // 2. Anything else, we read until whitespace while (true) { - if (reader.Peek() == -1) + int c = reader.Peek(); + if (c == -1) return false; + // Most calls land directly on a token: no whitespace or comment can start with these characters. + if (c > ' ' && c < 0x7F && c != '/') + return true; // If no whitespace was skipped and no comments were skipped, continue if (!SkipWhitespace(reader) && !SkipComments(reader)) return true; @@ -281,15 +301,14 @@ public static bool EvaluateConditional(Span str) { return false; } public static bool ReadConditional(KeyValuesReader reader, Span condition, out bool match) { - // Zero out if it's existing memory - for (int si = 0; si < condition.Length; si++) - condition[si] = '\0'; - if (reader.Peek() != '[') { match = false; return false; } + // Zero out if it's existing memory + condition.Clear(); + reader.Read(); // Determine if we're inverted. char c = (char)reader.Read(); @@ -356,9 +375,8 @@ public static bool HandleConditional(ReadOnlySpan condition, bool supporte return notSupported; } + // The reader must already be positioned on the key; both callers skip to parseable text first. private bool ReadKV(KeyValuesReader reader) { - SkipUntilParseableTextOrEOF(reader); - bool quoteTerminated = (char)reader.Peek() == '"'; string key = quoteTerminated ? ReadQuoteTerminatedString(reader, useEscapeSequences) : ReadWhitespaceTerminatedString(reader); @@ -367,10 +385,13 @@ private bool ReadKV(KeyValuesReader reader) { SkipUntilParseableTextOrEOF(reader); Span conditional = stackalloc char[16]; + int beforeConditional = reader.Position; bool isBlockConditional = ReadConditional(reader, conditional, out bool mustMatch); bool matches = isBlockConditional ? HandleConditional(conditional, mustMatch) : true; - SkipUntilParseableTextOrEOF(reader); + // Nothing was consumed unless there was a conditional, so there is nothing new to skip. + if (reader.Position != beforeConditional) + SkipUntilParseableTextOrEOF(reader); // Determine what we're reading next. // If we run into a {, we read another KVObject and set our value to that. @@ -433,25 +454,16 @@ private void ReadKVPairs(KeyValuesReader reader, bool matches) { // Returns true if we did anything at all to skip comments. public static bool SkipComments(KeyValuesReader reader) { - bool didAnything = false; - if (reader.Peek() == '/') { - // We need to check the stream for another / - reader.Read(); - if (reader.Peek() == '/') { // We got //, its a comment - // We read until the end of the line. - didAnything = true; - int val; - while ((val = reader.Read()) != -1) { - char c = (char)val; - if (c == '\n') - break; - } - } - else - reader.Unread('/'); - } + ReadOnlySpan text = reader.Remaining; + // We need to check the stream for another / + if (text.Length < 2 || text[0] != '/' || text[1] != '/') + return false; - return didAnything; + // We got //, its a comment + // We read until the end of the line. + int newline = text.IndexOf('\n'); + reader.Advance(newline == -1 ? text.Length : newline + 1); + return true; } private void DetermineValueType(string input) { @@ -516,29 +528,39 @@ static bool IsIntegerSyntax(ReadOnlySpan input) { } public static string ReadWhitespaceTerminatedString(KeyValuesReader reader) { - Span work = stackalloc char[1024]; - int i, len; - for (i = 0, len = work.Length; i < len; i++) { - int c = reader.Peek(); - if (c == -1) break; - char ch = (char)c; - if (char.IsWhiteSpace(ch)) break; - work[i] = ch; - reader.Read(); - } + ReadOnlySpan text = reader.Remaining; + int len = Math.Min(text.Length, MAX_TOKEN_LENGTH); + int i = 0; + while (i < len && !char.IsWhiteSpace(text[i])) + i++; - if (i >= len) { + if (i >= MAX_TOKEN_LENGTH) { Warning("KeyValues: string overflow, ignoring (should we allocate more space?)\n"); AssertMsg(false, "KeyValues string overflow"); } - return new(work[..i]); + return reader.ReadString(i); } + const int MAX_TOKEN_LENGTH = 1024; + public static string ReadQuoteTerminatedString(KeyValuesReader reader, bool useEscapeSequences) { int rd = reader.Read(); Debug.Assert(rd == '"', "invalid quote-terminated string"); - Span work = stackalloc char[1024]; + + // Nearly every string closes within the token limit and has nothing to unescape + ReadOnlySpan text = reader.Remaining; + if (text.Length > MAX_TOKEN_LENGTH) + text = text[..MAX_TOKEN_LENGTH]; + + int end = useEscapeSequences ? text.IndexOfAny('"', '\\') : text.IndexOf('"'); + if (end != -1 && text[end] == '"') { + string str = reader.ReadString(end); + reader.Advance(1); + return str; + } + + Span work = stackalloc char[MAX_TOKEN_LENGTH]; int i, len; bool lastCharacterWasEscape = false; int charI = 0; @@ -574,7 +596,7 @@ public static string ReadQuoteTerminatedString(KeyValuesReader reader, bool useE AssertMsg(false, "KeyValues string overflow"); } - return new(work[..i]); + return new(work[..charI]); } public bool LoadFromFile(string? filepath) { @@ -646,7 +668,7 @@ public KeyValues CreateNewKey() { Span buf = stackalloc char[12]; sprintf(buf, $"{newID}"); - return CreateKey(buf); + return CreateKey(((ReadOnlySpan)buf).SliceNullTerminatedString()); } public KeyValues CreateKey(ReadOnlySpan keyName) { diff --git a/Source.Common/IStudioRender.cs b/Source.Common/IStudioRender.cs index a01e3cf5e..d7fd8d065 100644 --- a/Source.Common/IStudioRender.cs +++ b/Source.Common/IStudioRender.cs @@ -108,6 +108,7 @@ public interface IStudioRender { void LockFlexWeights(int weightCount, out Span flexWeights, out Span flexDelayedWeights); void UnlockFlexWeights(); void DrawModel(ref DrawModelResults results, ref DrawModelInfo info, Span boneToWorld, Span flexWeights, Span flexDelayedWeights, in Vector3 modelOrigin, StudioRenderFlags flags = StudioRenderFlags.DrawEntireModel); + void DrawModelStaticProp(ref DrawModelInfo drawInfo, in Matrix3x4 modelToWorld, StudioRenderFlags flags = StudioRenderFlags.DrawEntireModel); void SetViewState(in Vector3 currentViewOrigin, in Vector3 currentViewRight, in Vector3 currentViewUp, in Vector3 currentViewForward); void SetColorModulation(Vector3 r_colormod); void SetAlphaModulation(float r_blend); diff --git a/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs b/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs index 2147b76a8..1f80b0be7 100644 --- a/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs +++ b/Source.Common/SoundEmitterSystem/ISoundEmitterSystemBase.cs @@ -5,7 +5,6 @@ using Source.Common.Utilities; using System; -using System.Collections.Frozen; using System.Collections.Generic; using System.Reflection.Emit; using System.Numerics; @@ -137,141 +136,197 @@ public bool Equals(SoundParametersInternal other) { } - // NOTE: Needs to reflect the soundlevel_t enum defined in soundflags.h - static readonly FrozenDictionary g_pSoundLevels = (new Dictionary { - { Audio.SoundLevel.LvlNone, "SNDLVL_NONE" }, - { Audio.SoundLevel.Lvl20dB, "SNDLVL_20dB" }, - { Audio.SoundLevel.Lvl25dB, "SNDLVL_25dB" }, - { Audio.SoundLevel.Lvl30dB, "SNDLVL_30dB" }, - { Audio.SoundLevel.Lvl35dB, "SNDLVL_35dB" }, - { Audio.SoundLevel.Lvl40dB, "SNDLVL_40dB" }, - { Audio.SoundLevel.Lvl45dB, "SNDLVL_45dB" }, - { Audio.SoundLevel.Lvl50dB, "SNDLVL_50dB" }, - { Audio.SoundLevel.Lvl55dB, "SNDLVL_55dB" }, - { Audio.SoundLevel.Lvl60dB, "SNDLVL_60dB" }, - { Audio.SoundLevel.Lvl65dB, "SNDLVL_65dB" }, - { Audio.SoundLevel.Lvl70dB, "SNDLVL_70dB" } , - { Audio.SoundLevel.Lvl75dB, "SNDLVL_75dB" } , - { Audio.SoundLevel.Lvl80dB, "SNDLVL_80dB" } , - { Audio.SoundLevel.Lvl85dB, "SNDLVL_85dB" } , - { Audio.SoundLevel.Lvl90dB, "SNDLVL_90dB" } , - { Audio.SoundLevel.Lvl95dB, "SNDLVL_95dB" } , - { Audio.SoundLevel.Lvl100dB, "SNDLVL_100dB" }, - { Audio.SoundLevel.Lvl105dB, "SNDLVL_105dB" }, - { Audio.SoundLevel.Lvl110dB, "SNDLVL_110dB" }, - { Audio.SoundLevel.Lvl120dB, "SNDLVL_120dB" }, - { Audio.SoundLevel.Lvl130dB, "SNDLVL_130dB" }, - { Audio.SoundLevel.Lvl140dB, "SNDLVL_140dB" }, - { Audio.SoundLevel.Lvl150dB, "SNDLVL_150dB" }, - { Audio.SoundLevel.Lvl180dB, "SNDLVL_180dB" }, - }).ToFrozenDictionary(); - - static readonly FrozenDictionary g_pSoundLevelsRev = (new Dictionary { - { "SNDLVL_NONE".Hash(invariant: true) , Audio.SoundLevel.LvlNone}, - { "SNDLVL_20dB".Hash(invariant: true) , Audio.SoundLevel.Lvl20dB}, - { "SNDLVL_25dB".Hash(invariant: true) , Audio.SoundLevel.Lvl25dB}, - { "SNDLVL_30dB".Hash(invariant: true) , Audio.SoundLevel.Lvl30dB}, - { "SNDLVL_35dB".Hash(invariant: true) , Audio.SoundLevel.Lvl35dB}, - { "SNDLVL_40dB".Hash(invariant: true) , Audio.SoundLevel.Lvl40dB}, - { "SNDLVL_45dB".Hash(invariant: true) , Audio.SoundLevel.Lvl45dB}, - { "SNDLVL_50dB".Hash(invariant: true) , Audio.SoundLevel.Lvl50dB}, - { "SNDLVL_55dB".Hash(invariant: true) , Audio.SoundLevel.Lvl55dB}, - { "SNDLVL_IDLE".Hash(invariant: true) , Audio.SoundLevel.LvlIdle}, - { "SNDLVL_TALKING".Hash(invariant: true) , Audio.SoundLevel.LvlTalking}, - { "SNDLVL_60dB".Hash(invariant: true) , Audio.SoundLevel.Lvl60dB}, - { "SNDLVL_65dB".Hash(invariant: true) , Audio.SoundLevel.Lvl65dB}, - { "SNDLVL_STATIC" .Hash(invariant: true) , Audio.SoundLevel.LvlStatic}, - { "SNDLVL_70dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl70dB} , - { "SNDLVL_NORM" .Hash(invariant: true) , Audio.SoundLevel.LvlNorm} , - { "SNDLVL_75dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl75dB} , - { "SNDLVL_80dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl80dB} , - { "SNDLVL_85dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl85dB} , - { "SNDLVL_90dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl90dB} , - { "SNDLVL_95dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl95dB} , - { "SNDLVL_100dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl100dB}, - { "SNDLVL_105dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl105dB}, - { "SNDLVL_110dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl110dB}, - { "SNDLVL_120dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl120dB}, - { "SNDLVL_130dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl130dB}, - { "SNDLVL_GUNFIRE".Hash(invariant: true) , Audio.SoundLevel.LvlGunfire}, - { "SNDLVL_140dB".Hash(invariant: true) , Audio.SoundLevel.Lvl140dB}, - { "SNDLVL_150dB".Hash(invariant: true) , Audio.SoundLevel.Lvl150dB}, - { "SNDLVL_180dB" .Hash(invariant: true) , Audio.SoundLevel.Lvl180dB}, - }).ToFrozenDictionary(); - - static readonly FrozenDictionary g_pChannelNames = (new Dictionary { - { SoundEntityChannel.Auto, "CHAN_AUTO" }, - { SoundEntityChannel.Weapon, "CHAN_WEAPON" }, - { SoundEntityChannel.Voice, "CHAN_VOICE" }, - { SoundEntityChannel.Item, "CHAN_ITEM" }, - { SoundEntityChannel.Body, "CHAN_BODY" }, - { SoundEntityChannel.Stream, "CHAN_STREAM" }, - { SoundEntityChannel.Static, "CHAN_STATIC" }, - { SoundEntityChannel.Voice2, "CHAN_VOICE2" }, - }).ToFrozenDictionary(); - - static readonly FrozenDictionary g_pChannelNamesRev = - g_pChannelNames - .Select(x => new KeyValuePair(x.Value.Hash(invariant: true), x.Key)) - .ToFrozenDictionary(); - - static readonly FrozenDictionary g_pVolumeLevels = (new Dictionary { - { VOL_NORM, "VOL_NORM" }, - }).ToFrozenDictionary(); - - static readonly FrozenDictionary g_pVolumeLevelsRev = - g_pVolumeLevels - .Select(x => new KeyValuePair(x.Value.Hash(invariant: false), x.Key)) - .ToFrozenDictionary(); - - static readonly FrozenDictionary g_pPitchLookup = (new Dictionary { - { PITCH_NORM, "PITCH_NORM" }, - { PITCH_LOW, "PITCH_LOW" }, - { PITCH_HIGH, "PITCH_HIGH" }, - }).ToFrozenDictionary(); - - static readonly FrozenDictionary g_pPitchLookupRev = - g_pPitchLookup - .Select(x => new KeyValuePair(x.Value.Hash(invariant: false), x.Key)) - .ToFrozenDictionary(); - public static ReadOnlySpan _SoundLevelToString(SoundLevel level) { - if (g_pSoundLevels.TryGetValue(level, out string? v)) - return v; + switch (level) { + case Audio.SoundLevel.LvlNone: return "SNDLVL_NONE"; + case Audio.SoundLevel.Lvl20dB: return "SNDLVL_20dB"; + case Audio.SoundLevel.Lvl25dB: return "SNDLVL_25dB"; + case Audio.SoundLevel.Lvl30dB: return "SNDLVL_30dB"; + case Audio.SoundLevel.Lvl35dB: return "SNDLVL_35dB"; + case Audio.SoundLevel.Lvl40dB: return "SNDLVL_40dB"; + case Audio.SoundLevel.Lvl45dB: return "SNDLVL_45dB"; + case Audio.SoundLevel.Lvl50dB: return "SNDLVL_50dB"; + case Audio.SoundLevel.Lvl55dB: return "SNDLVL_55dB"; + case Audio.SoundLevel.Lvl60dB: return "SNDLVL_60dB"; + case Audio.SoundLevel.Lvl65dB: return "SNDLVL_65dB"; + case Audio.SoundLevel.Lvl70dB: return "SNDLVL_70dB"; + case Audio.SoundLevel.Lvl75dB: return "SNDLVL_75dB"; + case Audio.SoundLevel.Lvl80dB: return "SNDLVL_80dB"; + case Audio.SoundLevel.Lvl85dB: return "SNDLVL_85dB"; + case Audio.SoundLevel.Lvl90dB: return "SNDLVL_90dB"; + case Audio.SoundLevel.Lvl95dB: return "SNDLVL_95dB"; + case Audio.SoundLevel.Lvl100dB: return "SNDLVL_100dB"; + case Audio.SoundLevel.Lvl105dB: return "SNDLVL_105dB"; + case Audio.SoundLevel.Lvl110dB: return "SNDLVL_110dB"; + case Audio.SoundLevel.Lvl120dB: return "SNDLVL_120dB"; + case Audio.SoundLevel.Lvl130dB: return "SNDLVL_130dB"; + case Audio.SoundLevel.Lvl140dB: return "SNDLVL_140dB"; + case Audio.SoundLevel.Lvl150dB: return "SNDLVL_150dB"; + case Audio.SoundLevel.Lvl180dB: return "SNDLVL_180dB"; + } return $"{level}"; } + static bool NamedSoundLevel(ReadOnlySpan name, out SoundLevel level) { + level = default; + if (name.IsEmpty) + return false; + + switch (name[0] | 0x20) { + case 'n': + if (0 == stricmp(name, "NORM")) { + level = Audio.SoundLevel.LvlNorm; + return true; + } + if (0 == stricmp(name, "NONE")) { + level = Audio.SoundLevel.LvlNone; + return true; + } + break; + case 'i': + if (0 == stricmp(name, "IDLE")) { + level = Audio.SoundLevel.LvlIdle; + return true; + } + break; + case 't': + if (0 == stricmp(name, "TALKING")) { + level = Audio.SoundLevel.LvlTalking; + return true; + } + break; + case 's': + if (0 == stricmp(name, "STATIC")) { + level = Audio.SoundLevel.LvlStatic; + return true; + } + break; + case 'g': + if (0 == stricmp(name, "GUNFIRE")) { + level = Audio.SoundLevel.LvlGunfire; + return true; + } + break; + } + + return false; + } + + static string? ChannelName(SoundEntityChannel channel) { + switch (channel) { + case SoundEntityChannel.Auto: return "CHAN_AUTO"; + case SoundEntityChannel.Weapon: return "CHAN_WEAPON"; + case SoundEntityChannel.Voice: return "CHAN_VOICE"; + case SoundEntityChannel.Item: return "CHAN_ITEM"; + case SoundEntityChannel.Body: return "CHAN_BODY"; + case SoundEntityChannel.Stream: return "CHAN_STREAM"; + case SoundEntityChannel.Static: return "CHAN_STATIC"; + case SoundEntityChannel.Voice2: return "CHAN_VOICE2"; + } + + return null; + } + + static bool NamedChannel(ReadOnlySpan name, out SoundEntityChannel channel) { + channel = default; + if (name.IsEmpty) + return false; + + switch (name[0] | 0x20) { + case 'a': + if (0 == stricmp(name, "AUTO")) { + channel = SoundEntityChannel.Auto; + return true; + } + break; + case 'w': + if (0 == stricmp(name, "WEAPON")) { + channel = SoundEntityChannel.Weapon; + return true; + } + break; + case 'v': + if (0 == stricmp(name, "VOICE")) { + channel = SoundEntityChannel.Voice; + return true; + } + if (0 == stricmp(name, "VOICE2")) { + channel = SoundEntityChannel.Voice2; + return true; + } + break; + case 'i': + if (0 == stricmp(name, "ITEM")) { + channel = SoundEntityChannel.Item; + return true; + } + break; + case 'b': + if (0 == stricmp(name, "BODY")) { + channel = SoundEntityChannel.Body; + return true; + } + break; + case 's': + if (0 == stricmp(name, "STREAM")) { + channel = SoundEntityChannel.Stream; + return true; + } + if (0 == stricmp(name, "STATIC")) { + channel = SoundEntityChannel.Static; + return true; + } + break; + } + + return false; + } + public static ReadOnlySpan _ChannelToString(int channel) { - if (g_pChannelNames.TryGetValue((SoundEntityChannel)channel, out string? v)) + string? v = ChannelName((SoundEntityChannel)channel); + if (v != null) return v; return $"{channel}"; } public static ReadOnlySpan _ChannelToString(SoundEntityChannel channel) { - if (g_pChannelNames.TryGetValue(channel, out string? v)) + string? v = ChannelName(channel); + if (v != null) return v; return $"{channel}"; } public static ReadOnlySpan _VolumeToString(float volume) { - if (g_pVolumeLevels.TryGetValue(volume, out string? v)) - return v; + if (volume == VOL_NORM) + return "VOL_NORM"; return $"{volume}"; } + static string? PitchName(byte pitch) { + switch (pitch) { + case PITCH_NORM: return "PITCH_NORM"; + case PITCH_LOW: return "PITCH_LOW"; + case PITCH_HIGH: return "PITCH_HIGH"; + } + + return null; + } + public static ReadOnlySpan _PitchToString(float pitch) { - if (g_pPitchLookup.TryGetValue((byte)pitch, out string? v)) + string? v = PitchName((byte)pitch); + if (v != null) return v; return $"{pitch}"; } public static ReadOnlySpan _PitchToString(byte pitch) { - if (g_pPitchLookup.TryGetValue(pitch, out string? v)) + string? v = PitchName(pitch); + if (v != null) return v; return $"{pitch}"; @@ -315,7 +370,7 @@ public SoundEntityChannel TextToChannel(ReadOnlySpan name) { if (strnicmp(name, "chan_", "chan_".Length) != 0) return (SoundEntityChannel)atoi(name); - if (g_pChannelNamesRev.TryGetValue(name.Hash(invariant: true), out SoundEntityChannel chan)) + if (NamedChannel(name["chan_".Length..], out SoundEntityChannel chan)) return chan; // At this point, it starts with chan_ but is not recognized @@ -336,11 +391,11 @@ public static SoundLevel TextToSoundLevel(ReadOnlySpan key) { return Audio.SoundLevel.LvlNorm; } - if (g_pSoundLevelsRev.TryGetValue(key.Hash(invariant: true), out SoundLevel level)) - return level; - if (0 == stricmp(key[..Math.Min((int)strlen(SNDLVL_PREFIX), key.Length)], SNDLVL_PREFIX)) { ReadOnlySpan val = key[(int)strlen(SNDLVL_PREFIX)..]; + if (NamedSoundLevel(val, out SoundLevel level)) + return level; + int sndlvl = atoi(val); if (sndlvl > 0 && sndlvl <= 180) return (SoundLevel)sndlvl; diff --git a/Source.Engine/EngineBuilder.cs b/Source.Engine/EngineBuilder.cs index 4b33582f9..a10e45c0b 100644 --- a/Source.Engine/EngineBuilder.cs +++ b/Source.Engine/EngineBuilder.cs @@ -18,6 +18,8 @@ using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; +using CvarRegistrar = System.Action>; + namespace Source.Engine; /// @@ -381,18 +383,105 @@ static IEnumerable safeTypeGet(Assembly assembly) { public static void ConVar_Register() { var engineAPI = Singleton(); ICvar cvar = engineAPI.GetRequiredService(); - var assemblies = AppDomain.CurrentDomain.GetAssemblies() - .Where(ReflectionUtils.IsSourceEngineAssembly); + Assembly[] assemblies = [.. AppDomain.CurrentDomain.GetAssemblies().Where(ReflectionUtils.IsSourceEngineAssembly)]; + + // Source.CodeAnalysis.ConVars emits Source.Generated.CvarRegistration into each Source assembly. Types it lists + // are registered through generated code; anything else falls back to the reflection path below. + Dictionary declared = []; + Dictionary inherited = []; + HashSet reflected = []; + HashSet generated = []; + foreach (var assembly in assemblies) { + if (LoadGeneratedCvarRegistration(assembly, declared, inherited, reflected)) + generated.Add(assembly); + } - Type CVAR = typeof(ConVar); - Type CCMD = typeof(ConCommand); + Func resolve = (type, concommand, name) => DetermineInstance(engineAPI, type, concommand, name)!; + List<(Type Declaring, CvarRegistrar Registrar)> registrars = []; foreach (var assembly in assemblies) { cvar.SetAssemblyIdentifier(assembly); + bool hasGenerated = generated.Contains(assembly); foreach (var type in assembly.GetTypes()) { if (type.IsAssignableTo(typeof(ConVar)) || type == typeof(ConVarRef)) continue; + if (!hasGenerated || !FindGeneratedCvarRegistrars(type, assemblies, generated, declared, inherited, reflected, registrars)) { + ConVar_RegisterByReflection(engineAPI, cvar, type); + continue; + } + + RunStaticConstructor(type); + + // Properties, then fields, then methods; the same order the reflection path uses. + for (int phase = 0; phase < 3 && registrars.Count > 0; phase++) { + foreach (var (declaring, registrar) in registrars) + registrar(phase, declaring, type, cvar, resolve); + } + } + } + } + + static bool LoadGeneratedCvarRegistration(Assembly assembly, Dictionary declared, Dictionary inherited, HashSet reflected) { + MethodInfo? populate = assembly.GetType("Source.Generated.CvarRegistration")?.GetMethod("Populate", BindingFlags.Public | BindingFlags.Static); + if (populate == null) + return false; + + Dictionary declaredNames = []; + Dictionary inheritedNames = []; + List reflectedNames = []; + populate.CreateDelegate, Dictionary, List>>()(declaredNames, inheritedNames, reflectedNames); + + foreach (var (name, registrar) in declaredNames) + declared.Add(assembly.GetType(name, true)!, registrar); + foreach (var (name, registrar) in inheritedNames) + inherited.Add(assembly.GetType(name, true)!, registrar); + foreach (string name in reflectedNames) + reflected.Add(assembly.GetType(name, true)!); + + return true; + } + + /// + /// Collects the generated registrars that apply to : its own, then those of each base type + /// (reflection returns inherited non-private instance members, so derived types register them too). + /// Returns false if the type has to go through reflection instead. + /// + static bool FindGeneratedCvarRegistrars(Type type, Assembly[] sourceAssemblies, HashSet generated, Dictionary declared, Dictionary inherited, HashSet reflected, List<(Type Declaring, CvarRegistrar Registrar)> registrars) { + registrars.Clear(); + if (reflected.Contains(type)) + return false; + + if (declared.TryGetValue(type, out CvarRegistrar? registrar)) + registrars.Add((type, registrar)); + + for (Type? baseType = type.BaseType; baseType != null; baseType = baseType.BaseType) { + Type key = baseType.IsConstructedGenericType ? baseType.GetGenericTypeDefinition() : baseType; + if (reflected.Contains(key)) + return false; + if (!generated.Contains(key.Assembly) && Array.IndexOf(sourceAssemblies, key.Assembly) != -1) + return false; + if (inherited.TryGetValue(key, out registrar)) + registrars.Add((key, registrar)); + } + + return true; + } + + static void RunStaticConstructor(Type type) { + try { + RuntimeHelpers.RunClassConstructor(type.TypeHandle); + } + catch (TypeInitializationException tie) when (tie.InnerException != null) { + ExceptionDispatchInfo.Capture(tie.InnerException).Throw(); + throw; + } + } + + static void ConVar_RegisterByReflection(IEngineAPI engineAPI, ICvar cvar, Type type) { + Type CVAR = typeof(ConVar); + { + { var props = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); diff --git a/Source.Engine/ImplStudio.cs b/Source.Engine/ImplStudio.cs index 335b60a75..e6e5a72ec 100644 --- a/Source.Engine/ImplStudio.cs +++ b/Source.Engine/ImplStudio.cs @@ -701,6 +701,289 @@ public void DrawModelExecute(ref DrawModelState state, ref ModelRenderInfo pInfo #endif } + struct RObject + { + public Matrix3x4 Matrix; + public IClientRenderable? Renderable; + public ColorMeshInfo[]? ColorMeshes; + public ITexture? EnvCubeMap; + public Vector3 LightingOrigin; + public short ModelIndex; + public short Lod; + public ModelInstanceHandle_t Instance; + public short Skin; + public short LightIndex; + } + + struct RModel + { + public Model? Model; + public StudioHeader? StudioHdr; + public StudioHWData? StudioHWData; + public float MaxArea; + public short LodStart; + public byte LodCount; + public bool VertexLit; + public bool NeedsCubemap; + public bool StaticLighting; + } + + int RObjectLess(RObject lhs, RObject rhs) { + Span models = CollectionsMarshal.AsSpan(ModelList); + if (lhs.ModelIndex == rhs.ModelIndex) { + if (lhs.Skin != rhs.Skin) + return lhs.Skin.CompareTo(rhs.Skin); + return lhs.Lod.CompareTo(rhs.Lod); + } + if (models[lhs.ModelIndex].MaxArea == models[rhs.ModelIndex].MaxArea) + return lhs.ModelIndex.CompareTo(rhs.ModelIndex); + return models[rhs.ModelIndex].MaxArea.CompareTo(models[lhs.ModelIndex].MaxArea); + } + + static int FindModel(List list, Model? model) { + for (int j = list.Count; --j >= 0;) { + if (list[j].Model == model) + return j; + } + return -1; + } + + readonly List ObjectList = new(1024); + readonly List ModelList = new(256); + readonly List LightObjects = new(256); + readonly List LightStates = new(256); + Comparison? ObjectLess; + + static readonly ConVar r_staticprop_lod = new("r_staticprop_lod", "-1"); + public int DrawStaticPropArrayFast(Span props, int count, bool shadowDepth) { +#if !SWDS + using MatRenderContextPtr renderContext = new(materials); + ObjectList.Clear(); + ModelList.Clear(); + LightObjects.Clear(); + LightStates.Clear(); + bool forceCubemap = r_showenvcubemap.GetBool(); + int drawnCount = 0; + int forcedLodSetting = r_lod.GetInt(); + if (r_staticprop_lod.GetInt() >= 0) + forcedLodSetting = r_staticprop_lod.GetInt(); + + for (int i = 0; i < count; i++) { + drawnCount++; + int modelIndex = FindModel(ModelList, props[i].Model); + if (modelIndex < 0) { + modelIndex = ModelList.Count; + ModelList.Add(new RModel { + Model = props[i].Model, + StudioHWData = MDLCache.GetHardwareData(props[i].Model!.Studio) + }); + } + if (ModelList[modelIndex].StudioHWData != null) { + RObject obj = default; + obj.Matrix = props[i].ModelToWorld; + obj.Renderable = props[i].Renderable; + obj.ModelIndex = (short)modelIndex; + obj.Instance = props[i].Instance; + obj.Skin = props[i].Skin; + obj.Lod = 0; + obj.ColorMeshes = null; + obj.EnvCubeMap = null; + obj.LightIndex = -1; + obj.LightingOrigin = props[i].LightingOrigin; + ObjectList.Add(obj); + } + } + + Span modelList = CollectionsMarshal.AsSpan(ModelList); + Span objectList = CollectionsMarshal.AsSpan(ObjectList); + + int lodStart = 0; + for (int i = 0; i < modelList.Length; i++) { + Model model = modelList[i].Model!; + ModelFlag flags = model.Flags; + modelList[i].StudioHdr = MDLCache.GetStudioHdr(model.Studio); + modelList[i].MaxArea = 1.0f; + modelList[i].LodStart = (short)lodStart; + modelList[i].LodCount = (byte)(modelList[i].StudioHWData != null ? modelList[i].StudioHWData!.NumLODs : 0); + bool bumpMapped = (flags & ModelFlag.UsesBumpMapping) != 0; + modelList[i].StaticLighting = ((modelList[i].StudioHdr!.Flags & StudioHdrFlags.StaticProp) != 0) && !bumpMapped; + modelList[i].VertexLit = (flags & ModelFlag.VertexLit) != 0; + modelList[i].NeedsCubemap = (flags & ModelFlag.UsesEnvCubemap) != 0; + + lodStart += modelList[i].LodCount; + } + + if (forcedLodSetting < 0) { + for (int i = 0; i < objectList.Length; i++) { + MathLib.MatrixGetColumn(in objectList[i].Matrix, 3, out Vector3 org); + float screenSize = renderContext.ComputePixelWidthOfSphere(org, 0.5f); + ref RModel model = ref modelList[objectList[i].ModelIndex]; + float metric = model.StudioHWData!.LODMetric(screenSize); + objectList[i].Lod = (short)model.StudioHWData.GetLODForMetric(metric); + if (objectList[i].Lod < model.StudioHWData.RootLOD) + objectList[i].Lod = (short)model.StudioHWData.RootLOD; + model.MaxArea = Math.Max(model.MaxArea, screenSize); + } + } + else { + for (int i = 0; i < objectList.Length; i++) { + ref RModel model = ref modelList[objectList[i].ModelIndex]; + objectList[i].Lod = (short)Math.Clamp(forcedLodSetting, model.StudioHWData!.RootLOD, model.LodCount - 1); + } + } + ObjectList.Sort(ObjectLess ??= RObjectLess); + + if (!shadowDepth) { + for (int i = 0; i < objectList.Length; i++) { + ref RObject obj = ref objectList[i]; + ref RModel model = ref modelList[obj.ModelIndex]; + bool staticLighting = model.StaticLighting && obj.Instance != MODEL_INSTANCE_INVALID; + bool vertexLit = model.VertexLit; + bool needsEnvCubemap = forceCubemap || model.NeedsCubemap; + // todo: decal light states, shadow objects + + DataCacheHandle_t colorMeshHandle = 0; + if (staticLighting) { + colorMeshHandle = ModelInstances[obj.Instance].ColorMeshHandle; + ColorMeshData? colorMeshData = CacheGet(colorMeshHandle); + if (colorMeshData == null || colorMeshData.NeedsRetry) { + if (UpdateStaticPropColorData(obj.Renderable!.GetIClientUnknown(), obj.Instance)) + colorMeshData = CacheGet(colorMeshHandle); + else if (colorMeshData == null || !colorMeshData.NeedsRetry) { + continue; + } + } + + if (colorMeshData != null && (colorMeshData.ColorMeshValid || colorMeshData.ColorTextureValid)) { + obj.ColorMeshes = colorMeshData.MeshInfos; + if (colorMeshData.ColorTextureValid && !colorMeshData.ColorTextureCreated) + CreateLightmapsFromData(colorMeshData); + } + else { + staticLighting = false; + } + } + + if (vertexLit || needsEnvCubemap) { + ref LightCacheHandle_t lightCache = ref Unsafe.NullRef(); + ITexture? envCubemapTexture = null; + if (obj.Instance != MODEL_INSTANCE_INVALID) { + if ((ModelInstances[obj.Instance].Flags & ModelInstanceFlags.HasStaticLighting) != 0 && ModelInstances[obj.Instance].LightCacheHandle != 0) + lightCache = ref ModelInstances[obj.Instance].LightCacheHandle; + } + + Assert(!Unsafe.IsNullRef(ref lightCache)); + LightingState lightingState = default; + ref LightingState state = ref lightingState; + if (!Unsafe.IsNullRef(ref lightCache)) { + if (!staticLighting || Render.StaticLightCacheAffectedByDynamicLight(lightCache)) { + state = ref Render.LightcacheGetStatic(lightCache, out envCubemapTexture, LightCacheFlags.Static | LightCacheFlags.Dynamic | LightCacheFlags.LightStyle); + Assert(state.NumLights >= 0 && state.NumLights <= Render.MAXLOCALLIGHTS); + } + else { + state = ref Render.LightcacheGetStatic(lightCache, out envCubemapTexture, LightCacheFlags.Dynamic | LightCacheFlags.LightStyle); + Assert(state.NumLights >= 0 && state.NumLights <= Render.MAXLOCALLIGHTS); + } + } + else { + Vector3 lightingOrigin = obj.LightingOrigin; + LightCacheFlags lightCacheFlags = staticLighting ? (LightCacheFlags.Dynamic | LightCacheFlags.LightStyle) + : (LightCacheFlags.Static | LightCacheFlags.Dynamic | LightCacheFlags.LightStyle | LightCacheFlags.AllowFast); + LightcacheGetDynamic_Stats stats = default; + envCubemapTexture = Render.LightcacheGetDynamic(lightingOrigin, ref lightingState, + ref stats, lightCacheFlags, false); + Assert(lightingState.NumLights >= 0 && lightingState.NumLights <= Render.MAXLOCALLIGHTS); + state = ref lightingState; + } + + if (needsEnvCubemap && envCubemapTexture != null) + obj.EnvCubeMap = envCubemapTexture; + + if (vertexLit) { + if (state.NumLights != 0 || state.HasAmbientColors()) { + obj.LightIndex = (short)LightStates.Count; + LightStates.Add(state); + LightObjects.Add((short)i); + } + } + } + } + } + StudioRender.SetColorModulation(new Vector3(1.0f, 1.0f, 1.0f)); + StudioRender.SetAlphaModulation(1.0f); + StudioRender.SetViewState(in Render.CurrentViewOrigin, in Render.CurrentViewRight, in Render.CurrentViewUp, in Render.CurrentViewForward); + + renderContext.MatrixMode(MaterialMatrixMode.Model); + renderContext.PushMatrix(); + renderContext.LoadIdentity(); + renderContext.DisableAllLocalLights(); + DrawModelInfo info = default; + for (int i = 0; i < 6; i++) + info.AmbientCube[i].Init(); + StudioRender.SetAmbientLightColors(info.AmbientCube); + renderContext.SetAmbientLight(0.0f, 0.0f, 0.0f); + info.NumLocalLights = 0; + info.StaticLighting = false; + + StudioRenderFlags drawFlags = StudioRenderFlags.DrawEntireModel | StudioRenderFlags.DrawStaticLighting; + if (shadowDepth) + drawFlags |= StudioRenderFlags.ShadowDepthTexture; + info.Body = 0; + info.HitboxSet = 0; + for (int i = 0; i < objectList.Length; i++) { + ref RObject obj = ref objectList[i]; + if (obj.LightIndex >= 0) + continue; + ref RModel model = ref modelList[obj.ModelIndex]; + if (obj.EnvCubeMap != null) + renderContext.BindLocalCubemap(obj.EnvCubeMap); + + info.StudioHdr = model.StudioHdr!; + info.HardwareData = model.StudioHWData!; + info.Skin = obj.Skin; + info.ClientEntity = obj.Renderable; + info.Lod = obj.Lod; + info.ColorMeshes = obj.ColorMeshes; + StudioRender.DrawModelStaticProp(ref info, in obj.Matrix, drawFlags); + } + + int localLightCount = 0; + InlineArray4 localLightDescs = default; + drawFlags = StudioRenderFlags.DrawEntireModel | StudioRenderFlags.DrawStaticLighting; + if (LightObjects.Count != 0) { + Span lightStates = CollectionsMarshal.AsSpan(LightStates); + for (int i = 0; i < LightObjects.Count; i++) { + ref RObject obj = ref objectList[LightObjects[i]]; + ref RModel model = ref modelList[obj.ModelIndex]; + + if (obj.EnvCubeMap != null) + renderContext.BindLocalCubemap(obj.EnvCubeMap); + + ref LightingState state = ref lightStates[obj.LightIndex]; + StudioRender.SetAmbientLightColors(state.BoxColor); + renderContext.SetLightingOrigin(obj.LightingOrigin); + R_SetNonAmbientLightingState(state.NumLights, state.LocalLight, out localLightCount, localLightDescs, true); + info.StudioHdr = model.StudioHdr!; + info.HardwareData = model.StudioHWData!; + info.Skin = obj.Skin; + info.ClientEntity = obj.Renderable; + info.Lod = obj.Lod; + info.ColorMeshes = obj.ColorMeshes; + StudioRender.DrawModelStaticProp(ref info, in obj.Matrix, drawFlags); + } + } + + // todo: shadow pass (DrawStaticPropShadows) + // todo: decal pass (DrawStaticPropDecals) + + renderContext.MatrixMode(MaterialMatrixMode.Model); + renderContext.PopMatrix(); + return drawnCount; +#else + return 0; +#endif + } + bool EngineLightingSuppressed = false; public void SuppressEngineLighting(bool suppress) => EngineLightingSuppressed = suppress; @@ -715,7 +998,8 @@ private void StudioSetupLighting(DrawModelState state, in Vector3 absEntCenter, #if !SWDS ITexture? envCubemapTexture = null; - LightingState lightingState = default; + LightingState lightingStateStorage = default; + ref LightingState lightingState = ref lightingStateStorage; Span saveLightPos = stackalloc Vector3[Render.MAXLOCALLIGHTS]; Vector3? debugLightingOrigin = null; @@ -747,9 +1031,9 @@ private void StudioSetupLighting(DrawModelState state, in Vector3 absEntCenter, if (staticLighting) { if (HardwareConfig.SupportsStaticPlusDynamicLighting()) { if (Render.StaticLightCacheAffectedByDynamicLight(lightcache)) - lightingState = Render.LightcacheGetStatic(lightcache, out envCubemapTexture); + lightingState = ref Render.LightcacheGetStatic(lightcache, out envCubemapTexture); else - lightingState = Render.LightcacheGetStatic(lightcache, out envCubemapTexture, LightCacheFlags.Dynamic | LightCacheFlags.LightStyle); + lightingState = ref Render.LightcacheGetStatic(lightcache, out envCubemapTexture, LightCacheFlags.Dynamic | LightCacheFlags.LightStyle); } else { if (Render.StaticLightCacheAffectedByDynamicLight(lightcache) || @@ -761,7 +1045,7 @@ private void StudioSetupLighting(DrawModelState state, in Vector3 absEntCenter, } if (!staticLighting) - lightingState = Render.LightcacheGetStatic(lightcache, out envCubemapTexture); + lightingState = ref Render.LightcacheGetStatic(lightcache, out envCubemapTexture); if (r_decalstaticprops.GetBool() && modelInst != null && drawInfo.StaticLighting && hasDecals) { for (int iCube = 0; iCube < 6; ++iCube) diff --git a/Source.Engine/Lightcache.cs b/Source.Engine/Lightcache.cs index c207639d5..5bd9c4841 100644 --- a/Source.Engine/Lightcache.cs +++ b/Source.Engine/Lightcache.cs @@ -1246,7 +1246,8 @@ private ReadOnlySpan ComputeDynamicLighting(LightCache cache, scoped ref L } Assert(cache.DynamicLightingState.NumLights >= 0 && cache.DynamicLightingState.NumLights <= MAXLOCALLIGHTS); - lightingState = cache.DynamicLightingState; + if (!Unsafe.AreSame(ref lightingState, ref cache.DynamicLightingState)) + lightingState = cache.DynamicLightingState; return vis; } diff --git a/Source.Engine/StaticPropMgr.cs b/Source.Engine/StaticPropMgr.cs index af71fd46c..e2d4e8eb3 100644 --- a/Source.Engine/StaticPropMgr.cs +++ b/Source.Engine/StaticPropMgr.cs @@ -341,8 +341,17 @@ public void DrawStaticProps(ref InlineArray512 props, int cou if (!r_drawstaticprops.GetBool()) return; - // todo: fast pipeline - DrawStaticProps_Slow(ref props, count, shadowDepth, drawVCollideWireframe); + if (IsUsingStaticPropDebugModes() || drawVCollideWireframe) + DrawStaticProps_Slow(ref props, count, shadowDepth, drawVCollideWireframe); + else { + if (pipeline_static_props.GetBool() && + HardwareConfig.GetDXSupportLevel() >= 80 && + HardwareConfig.SupportsColorOnSecondStream() && + HardwareConfig.SupportsStaticPlusDynamicLighting()) + DrawStaticProps_FastPipeline(props, count, shadowDepth); + else + DrawStaticProps_Fast(ref props, count, shadowDepth); + } } void DrawStaticProps_Slow(ref InlineArray512 props, int count, bool shadowDepth, bool drawVCollideWireframe) { @@ -359,7 +368,32 @@ void DrawStaticProps_Slow(ref InlineArray512 props, int count } void DrawStaticProps_Fast(ref InlineArray512 props, int count, bool shadowDepth) => throw new NotImplementedException(); - void DrawStaticProps_FastPipeline(ref InlineArray512 props, int count, bool shadowDepth) => throw new NotImplementedException(); + + const int MAX_OBJECTS = 2048; + readonly StaticPropRenderInfo[] PropList = new StaticPropRenderInfo[MAX_OBJECTS]; + + void DrawStaticProps_FastPipeline(Span props, int count, bool shadowDepth) { + Span propList = PropList; + int listCount = 0; + if (count > MAX_OBJECTS) + DrawStaticProps_FastPipeline(props[MAX_OBJECTS..], count - MAX_OBJECTS, shadowDepth); + + for (int i = 0; i < count; i++) { + StaticProp prop = (StaticProp)props[i]; + if (prop.Model == null) + continue; + propList[listCount].ModelToWorld = prop.ModelToWorld; + propList[listCount].Model = prop.Model; + propList[listCount].Instance = prop.ModelInstance; + propList[listCount].Skin = prop.Skin; + propList[listCount].Renderable = prop; + propList[listCount].LightingOrigin = prop.LightingOrigin; + listCount++; + } + modelrender.DrawStaticPropArrayFast(propList, listCount, shadowDepth); + } + + static readonly ConVar pipeline_static_props = new("pipeline_static_props", "1"); void OutputLevelStats() => throw new NotImplementedException(); void PrecacheLighting() { // FIXME: This is very slow... @@ -593,12 +627,12 @@ public class StaticProp : IClientUnknown, IClientRenderable, ICollideable Vector3 Origin; QAngle Angles; - Model? Model; + internal Model? Model; SpatialPartitionHandle_t Partition; - ModelInstanceHandle_t ModelInstance; + internal ModelInstanceHandle_t ModelInstance; byte Alpha; byte Solid; - byte Skin; + internal byte Skin; byte flags; ushort firstLeaf; ushort leafCount; @@ -609,13 +643,13 @@ public class StaticProp : IClientUnknown, IClientRenderable, ICollideable Vector3 RenderBBoxMin; Vector3 RenderBBoxMax; - Matrix3x4 ModelToWorld; + internal Matrix3x4 ModelToWorld; float radius; Vector3 WorldRenderBBoxMin; Vector3 WorldRenderBBoxMax; - Vector3 LightingOrigin; + internal Vector3 LightingOrigin; public StaticProp() { ModelInstance = MODEL_INSTANCE_INVALID; diff --git a/Source.GUI.Controls/AnimationController.cs b/Source.GUI.Controls/AnimationController.cs index bd84076b4..636779402 100644 --- a/Source.GUI.Controls/AnimationController.cs +++ b/Source.GUI.Controls/AnimationController.cs @@ -7,7 +7,6 @@ using Source.Common.Mathematics; using Source.Common.Utilities; -using System.Collections.Frozen; using System.Text; using static Source.GUI.Controls.AnimationController; @@ -1192,30 +1191,26 @@ private void SetupPosition(ref AnimCmdAnimate cmd, ref float output, ReadOnlySpa output = (float)pos; } - static readonly FrozenDictionary g_AlignmentLookup = new Dictionary(){ - { "northwest".Hash(), Alignment.Northwest }, - { "north".Hash(), Alignment.North }, - { "northeast".Hash(), Alignment.Northeast }, - { "west".Hash(), Alignment.West }, - { "center".Hash(), Alignment.Center }, - { "east".Hash(), Alignment.East }, - { "southwest".Hash(), Alignment.Southwest }, - { "south".Hash(), Alignment.South }, - { "southeast".Hash(), Alignment.Southeast }, - { "nw".Hash(), Alignment.Northwest }, - { "n".Hash(), Alignment.North }, - { "ne".Hash(), Alignment.Northeast }, - { "w".Hash(), Alignment.West }, - { "c".Hash(), Alignment.Center }, - { "e".Hash(), Alignment.East }, - { "sw".Hash(), Alignment.Southwest }, - { "s".Hash(), Alignment.South }, - { "se".Hash(), Alignment.Southeast }, - }.ToFrozenDictionary(); - private static Alignment LookupAlignment(ReadOnlySpan sz) { sz = sz.SliceNullTerminatedString(); - return g_AlignmentLookup.TryGetValue(sz.Hash(), out Alignment al) ? al : Alignment.Northwest; + if (sz.Length > "northwest".Length) + return Alignment.Northwest; + + Span lower = stackalloc char[sz.Length]; + sz.ToLowerInvariant(lower); + switch (lower) { + case "northwest": case "nw": return Alignment.Northwest; + case "north": case "n": return Alignment.North; + case "northeast": case "ne": return Alignment.Northeast; + case "west": case "w": return Alignment.West; + case "center": case "c": return Alignment.Center; + case "east": case "e": return Alignment.East; + case "southwest": case "sw": return Alignment.Southwest; + case "south": case "s": return Alignment.South; + case "southeast": case "se": return Alignment.Southeast; + } + + return Alignment.Northwest; } private bool ParseScriptFile(ReadOnlySpan mem) { diff --git a/Source.NET.slnx b/Source.NET.slnx index ea8b4b413..b0328a0f5 100644 --- a/Source.NET.slnx +++ b/Source.NET.slnx @@ -22,6 +22,9 @@ + + + diff --git a/Source.ShaderAPI.Gl46/PersistentBufferRing.cs b/Source.ShaderAPI.Gl46/PersistentBufferRing.cs new file mode 100644 index 000000000..4fe3836a7 --- /dev/null +++ b/Source.ShaderAPI.Gl46/PersistentBufferRing.cs @@ -0,0 +1,84 @@ +namespace Source.ShaderAPI.Gl46; + +public unsafe class PersistentBufferRing : IDisposable +{ + const int MEMORY_BUDGET = 48 * 1024 * 1024; + const int MIN_SLOTS = 8; + const int MAX_SLOTS = 256; + const ulong WAIT_TIMEOUT_NS = 1_000_000_000; + + sealed class Slot + { + public uint Buffer; + public byte* Memory; + public void* Fence; + } + + readonly List Slots = []; + readonly int Size; + readonly int MaxSlots; + int Current; + + public PersistentBufferRing(int size) { + Size = size; + MaxSlots = Math.Clamp(MEMORY_BUDGET / Math.Max(size, 1), MIN_SLOTS, MAX_SLOTS); + Slots.Add(CreateSlot()); + } + + public uint Buffer => Slots[Current].Buffer; + public byte* Memory => Slots[Current].Memory; + + Slot CreateSlot() { + Slot slot = new(); + slot.Buffer = glCreateBuffer(); + glNamedBufferStorage(slot.Buffer, Size, null, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + slot.Memory = (byte*)glMapNamedBufferRange(slot.Buffer, 0, Size, GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT); + return slot; + } + + static bool IsSignaled(Slot slot, uint flags, ulong timeout) { + if (slot.Fence == null) + return true; + + if (glClientWaitSync(slot.Fence, flags, timeout) == GL_TIMEOUT_EXPIRED) + return false; + + glDeleteSync(slot.Fence); + slot.Fence = null; + return true; + } + + public void Advance() { + Slot slot = Slots[Current]; + if (slot.Fence != null) + glDeleteSync(slot.Fence); + slot.Fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + + int next = (Current + 1) % Slots.Count; + if (next != Current && IsSignaled(Slots[next], 0, 0)) { + Current = next; + return; + } + + if (Slots.Count < MaxSlots) { + Slots.Insert(Current + 1, CreateSlot()); + Current++; + return; + } + + while (!IsSignaled(Slots[next], GL_SYNC_FLUSH_COMMANDS_BIT, WAIT_TIMEOUT_NS)) ; + Current = next; + } + + public void Dispose() { + foreach (Slot slot in Slots) { + if (slot.Fence != null) + glDeleteSync(slot.Fence); + glUnmapNamedBuffer(slot.Buffer); + uint buffer = slot.Buffer; + glDeleteBuffers(1, &buffer); + } + Slots.Clear(); + Current = 0; + } +} diff --git a/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs b/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs index ba723304e..acc23bdf8 100644 --- a/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs +++ b/Source.ShaderAPI.Gl46/ShaderAPIGl46.cs @@ -148,12 +148,6 @@ public unsafe void ClearBuffers(bool clearColor, bool clearDepth, bool clearSten glStencilMask(0xFF); if (flags != 0) { - bool* colorMask = stackalloc bool[4]; - glGetBooleanv(GL_COLOR_WRITEMASK, colorMask); - bool scissorWasEnabled = glIsEnabled(GL_SCISSOR_TEST); - if (clearColor) - glColorMask(true, true, true, true); - bool renderTargetMatchesViewport = (renderTargetWidth == -1 && renderTargetHeight == -1) || (renderTargetWidth == Viewport.Width && renderTargetHeight == Viewport.Height); @@ -169,10 +163,8 @@ public unsafe void ClearBuffers(bool clearColor, bool clearDepth, bool clearSten glDisable(GL_SCISSOR_TEST); } - if (scissorWasEnabled) + if (ScissorEnabled) glEnable(GL_SCISSOR_TEST); - if (clearColor) - glColorMask(colorMask[0], colorMask[1], colorMask[2], colorMask[3]); } if (boardStateValid) { @@ -578,8 +570,11 @@ public void SetStencilWriteMask(uint msk) { void ApplyStencilFunc() => glStencilFunc(StencilCompareFunction.GLEnum(), StencilReferenceValue, StencilTestMask); + bool ScissorEnabled; + public void SetScissorRect(int left, int top, int right, int bottom, bool enableScissor) { FlushBufferedPrimitives(); + ScissorEnabled = enableScissor; if (!enableScissor) { glDisable(GL_SCISSOR_TEST); return; @@ -1934,7 +1929,7 @@ public void GMOD_ForceFilterMode(bool min, int mode) { ForcedMagFilter = mode; } - unsafe void ApplyForcedFilter(Sampler sampler, ShaderAPITextureHandle_t textureHandle) { + void ApplyForcedFilter(Sampler sampler, ShaderAPITextureHandle_t textureHandle) { int unit = (int)sampler; if (ForcedMinFilter == 0 && ForcedMagFilter == 0) { if (ForcedFilterSamplerBound[unit]) { @@ -1947,20 +1942,19 @@ unsafe void ApplyForcedFilter(Sampler sampler, ShaderAPITextureHandle_t textureH if (ForcedFilterSamplers[unit] == 0) ForcedFilterSamplers[unit] = glCreateSamplers(); uint samplerObject = ForcedFilterSamplers[unit]; - uint texture = GetGL46Texture(textureHandle); - - int value; - foreach (int pname in (ReadOnlySpan)[GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T, GL_TEXTURE_WRAP_R, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_MAG_FILTER]) { - glGetTextureParameteriv(texture, pname, &value); - glSamplerParameteri(samplerObject, pname, value); - } - if (SupportsSRGBDecode()) { - glGetTextureParameteriv(texture, GL_TEXTURE_SRGB_DECODE_EXT, &value); - glSamplerParameteri(samplerObject, GL_TEXTURE_SRGB_DECODE_EXT, value); - } + InternalTextureInfo info = GetTexture(textureHandle); + ref TextureSamplerParams samplerParams = ref info.GetSamplerParams(); + + glSamplerParameteri(samplerObject, GL_TEXTURE_WRAP_S, samplerParams.WrapS); + glSamplerParameteri(samplerObject, GL_TEXTURE_WRAP_T, samplerParams.WrapT); + glSamplerParameteri(samplerObject, GL_TEXTURE_WRAP_R, samplerParams.WrapR); + glSamplerParameteri(samplerObject, GL_TEXTURE_MIN_FILTER, samplerParams.MinFilter); + glSamplerParameteri(samplerObject, GL_TEXTURE_MAG_FILTER, samplerParams.MagFilter); + if (SupportsSRGBDecode()) + glSamplerParameteri(samplerObject, GL_TEXTURE_SRGB_DECODE_EXT, info.LastSRGBDecode); glSamplerParameterf(samplerObject, GL_TEXTURE_MAX_ANISOTROPY, 1.0f); - bool mipped = GetTexture(textureHandle).Levels > 1; + bool mipped = info.Levels > 1; switch (ForcedMinFilter) { case 0: break; @@ -2214,6 +2208,7 @@ public unsafe void CreateTextures( else { texture.NumCopies = (byte)copies; texture.TextureCopies = new uint[copies]; + texture.SamplerParams = new TextureSamplerParams[copies]; for (int k = 0; k < copies; k++) { uint glTex = glCreateTexture(texture.DetermineGLObjectType()); glTextureStorage2D(glTex, mipCount, ImageLoader.GetGLImageInternalFormat(dstFormat, isSRGB), width, height); @@ -2275,9 +2270,27 @@ public ShaderAPITextureHandle_t CreateTextureHandle() { return handle; } + public struct TextureSamplerParams + { + public int WrapS; + public int WrapT; + public int WrapR; + public int MinFilter; + public int MagFilter; + + public static TextureSamplerParams Default => new() { + WrapS = GL_REPEAT, + WrapT = GL_REPEAT, + WrapR = GL_REPEAT, + MinFilter = GL_NEAREST_MIPMAP_LINEAR, + MagFilter = GL_LINEAR + }; + } + public class InternalTextureInfo { internal uint[] TextureCopies = [0]; + internal TextureSamplerParams[] SamplerParams = [TextureSamplerParams.Default]; internal readonly uint DepthStencilSurface = 0; internal readonly InlineArray2 RenderTargetSurface; @@ -2327,12 +2340,16 @@ public uint[] GetTextureArray() { public void SetTexture(uint tex) { TextureCopies[0] = tex; + SamplerParams[0] = TextureSamplerParams.Default; } public void SetTexture(int copy, uint tex) { TextureCopies[copy] = tex; + SamplerParams[copy] = TextureSamplerParams.Default; } + public ref TextureSamplerParams GetSamplerParams() => ref SamplerParams[NumCopies == 1 ? 0 : CurrentCopy]; + internal nuint GetMemUsage() { return SizeBytes; } @@ -2832,58 +2849,73 @@ public void TexWrap(TexCoordComponent coord, TexWrapMode wrapMode) { return; } + int wrap; switch (wrapMode) { case TexWrapMode.Clamp: - glTextureParameteri(GetModifyTexture(), coordinate, GL_CLAMP_TO_EDGE); + wrap = GL_CLAMP_TO_EDGE; break; case TexWrapMode.Repeat: - glTextureParameteri(GetModifyTexture(), coordinate, GL_REPEAT); + wrap = GL_REPEAT; break; case TexWrapMode.Border: - glTextureParameteri(GetModifyTexture(), coordinate, GL_CLAMP_TO_BORDER); + wrap = GL_CLAMP_TO_BORDER; break; default: Warning("ShaderAPIGl46.TexWrap: unknown wrapMode\n"); - break; + return; + } + + glTextureParameteri(GetModifyTexture(), coordinate, wrap); + ref TextureSamplerParams samplerParams = ref GetTexture(ModifyTextureHandle).GetSamplerParams(); + switch (coord) { + case TexCoordComponent.S: samplerParams.WrapS = wrap; break; + case TexCoordComponent.T: samplerParams.WrapT = wrap; break; + case TexCoordComponent.U: samplerParams.WrapR = wrap; break; } } IMaterialSystemHardwareConfig HardwareConfig = Singleton(); public void TexMinFilter(TexFilterMode mode) { + int filter; switch (mode) { case TexFilterMode.Nearest: - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_NEAREST); + filter = GL_NEAREST; break; case TexFilterMode.Linear: - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR); + filter = GL_LINEAR; break; case TexFilterMode.NearestMipmapNearest: - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + filter = GL_NEAREST_MIPMAP_NEAREST; break; case TexFilterMode.LinearMipmapNearest: - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); + filter = GL_LINEAR_MIPMAP_NEAREST; break; case TexFilterMode.NearestMipmapLinear: - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR); + filter = GL_NEAREST_MIPMAP_LINEAR; break; case TexFilterMode.LinearMipmapLinear: - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + filter = GL_LINEAR_MIPMAP_LINEAR; break; case TexFilterMode.Anisotropic: glTextureParameterf(GetModifyTexture(), GL_TEXTURE_MAX_ANISOTROPY, AnisotropicLevel); - glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + filter = GL_LINEAR_MIPMAP_LINEAR; break; default: - break; + return; } + + glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MIN_FILTER, filter); + GetTexture(ModifyTextureHandle).GetSamplerParams().MinFilter = filter; } public void TexMagFilter(TexFilterMode mode) { switch (mode) { case TexFilterMode.Nearest: glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MAG_FILTER, GL_NEAREST); + GetTexture(ModifyTextureHandle).GetSamplerParams().MagFilter = GL_NEAREST; break; case TexFilterMode.Linear: glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MAG_FILTER, GL_LINEAR); + GetTexture(ModifyTextureHandle).GetSamplerParams().MagFilter = GL_LINEAR; break; case TexFilterMode.NearestMipmapNearest: Warning("ShaderAPIGl46.TexMagFilter: TexFilterMode.NearestMipmapNearest is invalid\n"); @@ -2899,6 +2931,7 @@ public void TexMagFilter(TexFilterMode mode) { break; case TexFilterMode.Anisotropic: glTextureParameteri(GetModifyTexture(), GL_TEXTURE_MAG_FILTER, GL_LINEAR); + GetTexture(ModifyTextureHandle).GetSamplerParams().MagFilter = GL_LINEAR; glTextureParameterf(GetModifyTexture(), GL_TEXTURE_MAX_ANISOTROPY_EXT, AnisotropicLevel); break; default: diff --git a/Source.ShaderAPI.Gl46/VertexBufferGl46.cs b/Source.ShaderAPI.Gl46/VertexBufferGl46.cs index 18ae5e86f..810940e0a 100644 --- a/Source.ShaderAPI.Gl46/VertexBufferGl46.cs +++ b/Source.ShaderAPI.Gl46/VertexBufferGl46.cs @@ -21,10 +21,12 @@ public unsafe class VertexBufferGl46 : IDisposable internal bool ExternalMemory; internal bool SoftwareVertexProcessing; internal bool LateCreateShouldDiscard; + internal bool Written; int vbo = -1; + PersistentBufferRing? ring; - internal uint VBO() => vbo > 0 ? (uint)vbo : throw new NullReferenceException("Vertex Buffer Object was null"); + internal uint VBO() => ring != null ? ring.Buffer : vbo > 0 ? (uint)vbo : throw new NullReferenceException("Vertex Buffer Object was null"); public VertexBufferGl46(bool dynamic) { Dynamic = dynamic; @@ -134,6 +136,16 @@ internal void ChangeConfiguration(VertexFormat format, int vertexSize, int total int lastBufferSize = -1; public void RecomputeVBO() { + if (Dynamic) { + if (BufferSize > lastBufferSize) { + lastBufferSize = BufferSize; + ring?.Dispose(); + ring = new PersistentBufferRing(BufferSize); + Written = false; + } + return; + } + // Create the VBO if it doesn't exist if (vbo == -1) vbo = (int)glCreateBuffer(); @@ -175,11 +187,22 @@ public void RecomputeVBO() { int lockOffset = NextLockOffset(); baseVertexIndex = VertexSize == 0 ? 0 : (lockOffset / VertexSize); + if (Dynamic) { + if (ring == null) + RecomputeVBO(); + else if (discard && Written) + ring.Advance(); + + if (discard) + Written = false; + + Locked = true; + Position = lockOffset; + return ring!.Memory + lockOffset; + } + if (SysmemBuffer == null) RecomputeVBO(); - else if (discard) - glNamedBufferData((uint)vbo, BufferSize, null, GL_DYNAMIC_DRAW); - Locked = true; Position = lockOffset; @@ -193,8 +216,12 @@ public void Unlock(int vertexCount) { int lockOffset = NextLockOffset(); int bufferSize = vertexCount * VertexSize; - if (bufferSize > 0) - glNamedBufferSubData((uint)vbo, lockOffset, bufferSize, (byte*)SysmemBuffer + lockOffset); + if (bufferSize > 0) { + if (Dynamic) + Written = true; + else + glNamedBufferSubData((uint)vbo, lockOffset, bufferSize, (byte*)SysmemBuffer + lockOffset); + } Position = lockOffset + bufferSize; Locked = false; } @@ -204,11 +231,19 @@ public void Unlock(int vertexCount) { public byte* ModifyLock(int firstVertex, int numVerts, out int baseVertexIndex) { Assert(!Locked); + modifyOffset = firstVertex * VertexSize; + baseVertexIndex = firstVertex; + if (Dynamic) { + if (ring == null) + RecomputeVBO(); + + Locked = true; + return ring!.Memory + modifyOffset; + } + if (SysmemBuffer == null) RecomputeVBO(); - modifyOffset = firstVertex * VertexSize; - baseVertexIndex = firstVertex; Locked = true; return (byte*)SysmemBuffer + modifyOffset; } @@ -217,8 +252,12 @@ public void ModifyUnlock(int vertexCount) { if (!Locked) return; - if (vertexCount > 0) - glNamedBufferSubData((uint)vbo, modifyOffset, vertexCount * VertexSize, (byte*)SysmemBuffer + modifyOffset); + if (vertexCount > 0) { + if (Dynamic) + Written = true; + else + glNamedBufferSubData((uint)vbo, modifyOffset, vertexCount * VertexSize, (byte*)SysmemBuffer + modifyOffset); + } Locked = false; } @@ -362,6 +401,12 @@ public static unsafe void ComputeVertexDescription(byte* vertexMemory, VertexFor } public void Dispose() { + if (ring != null) { + ring.Dispose(); + ring = null; + lastBufferSize = -1; + } + if (vbo != -1) { Assert(SysmemBuffer != null); fixed (int* ugh = &vbo) diff --git a/Source.StudioRender/StudioRender.cs b/Source.StudioRender/StudioRender.cs index aa802dee0..63b248fe3 100644 --- a/Source.StudioRender/StudioRender.cs +++ b/Source.StudioRender/StudioRender.cs @@ -130,6 +130,44 @@ internal void DrawModel(ref DrawModelInfo info, StudioRenderCtx RC, Span poseToWorld, StudioHeader studioHdr, int boneMask, in Vector3 viewOrigin, Matrix3x4* pBoneToWorld) { if ((studioHdr.Flags & StudioHdrFlags.StaticProp) != 0) { poseToWorld[0] = pBoneToWorld[0]; diff --git a/Source.StudioRender/StudioRenderContext.cs b/Source.StudioRender/StudioRenderContext.cs index 98eb25fbc..fb9873e47 100644 --- a/Source.StudioRender/StudioRenderContext.cs +++ b/Source.StudioRender/StudioRenderContext.cs @@ -731,6 +731,13 @@ public void DrawModel(ref DrawModelResults results, ref DrawModelInfo info, Span studioRenderImp.DrawModel(ref info, RC, boneToWorld, flexWeightsOrZero, flexDelayedWeightsOrWeights, flags); } + public void DrawModelStaticProp(ref DrawModelInfo info, in Matrix3x4 modelToWorld, StudioRenderFlags flags = StudioRenderFlags.DrawEntireModel) { + if (info.Lod < info.HardwareData.RootLOD) + info.Lod = info.HardwareData.RootLOD; + + studioRenderImp.DrawModelStaticProp(ref info, RC, in modelToWorld, flags); + } + private void InvokeBindProxies(ref DrawModelInfo info) { if (RC.ForcedMaterial != null) { if (RC.ForcedMaterialType == OverrideType.Normal && RC.ForcedMaterial.HasProxy()) {