chore(history): squash 59 commit(s) from 2026-09-23

- refactor(hud/weather/render): Scoreboard 931→240 + Rain 903→159 + UiMeshGeometry 956→99 (8.5.2)
- refactor(chams): Chams 985 → 129 + 4×≤195 (8.5.2)
- refactor(module): Module 1031 → 139 + 4×≤134 (8.5.2)
- refactor(tooltips): BetterTooltips 960 → 181 + 4×≤199 (8.5.2)
- refactor(clickgui): ClickGuiPickerState 970→166 + ModulesMenuScreen 966→145 (8.5.2)
- refactor(clickgui): CombatProtocolHeuristicsEditorState 932→79 + 4×≤192 (8.5.2)
- refactor(hud): CompactHudStatModel 894 → 125 + 4×≤175 (8.5.2)
- refactor(iris): ShaderPatchCompiler 843→66 + 4×≤174 (8.5.2)
- refactor(tab): CustomTabList 834→136 + 4×≤166 (8.5.2)
- refactor(relations): OnlineRelationPlayerPickerComponent 837→74 + 4×≤192 (8.5.2)
- refactor(hits): HitEffect 770→120 + 4×≤176 (8.5.2)
- refactor(predict): Predictions 815→150 + 4×≤195 (8.5.2)
- refactor(config): ConfigProfilesComponent 799→125 + 4×≤195 (8.5.2)
- refactor(sim): PlayerMovementSimulation 757→157 + 4×≤194 (8.5.2)
- refactor(predict): ProjectilePuncher 747→104 + 3×≤134 (8.5.2)
- refactor(crosshair): Crosshair 743→143 + 4×≤167 (8.5.2)
- refactor(mediaplayer): MediaPlayer 743→174 + 4×≤196 (8.5.2)
- refactor(hud): Itemizer 699→193 + Cooldowns 707→151 (8.5.2)
- refactor(hud): Potions 617→176 + 4×≤166 (8.5.2)
- refactor(hud): Admins 702→118 + 4×≤138 (8.5.2)
- refactor(hud): Keybinds 611→157 + 4×≤160 (8.5.2)
- refactor(hud): ModuleList 499→181 + 3×≤150 (8.5.2)
- refactor(hud): Radar 392→161 + 2×≤124 (8.5.2)
- refactor(hud): Fps 309→168 + 3×≤107 (8.5.2)
- refactor(hud): Memory 304→151 + 3×≤144 (8.5.2)
- refactor(hud): Tps 339→154 + 3×≤143 (8.5.2)
- refactor(hud): Ping 309→145 + 3×≤125 (8.5.2)
- refactor(hud): SpeedBps 334→156 + 3×≤142 (8.5.2)
- refactor(hud): GameTime 314→150 + 3×≤127 (8.5.2)
- refactor(hud): SystemTime 300→136 + 3×≤130 (8.5.2)
- refactor(hud): Coordinates 337→174 + 2×≤172 (8.5.2)
- refactor(hud): Armor 324→162 + 2×≤148 (8.5.2)
- refactor(hud): Inventory 476→153 + 3×≤181 (8.5.2)
- docs(todo): обновлён 8.5.2 — отмечены готовые гиганты (BetterButtons/InventorySwap/Svg/Темы/WorldParticles/Module/Chams и хвост)
- docs(api): папка combatant-client-26.2/docs + addons.md (API можно ломать при рефакторе)
- refactor(visuals): BlockHighlight 692→271 + 4×≤200 (8.5.2)
- refactor(mixins): GameRendererMixin 830→344 (хуки) + 4 хелпера (8.5.2)
- refactor(visuals): WorldParticles 821→260 + 6×≤200, режимы добиты (8.5.2)
- docs(todo): 8.5.2 — WorldParticles/GameRendererMixin/BlockHighlight отмечены ГОТОВО
- refactor(render): MeshBuilder 738→447 (фасад write-API) + 3 хелпера, FrameStats снесён (8.5.2)
- docs(todo): Фаза 10 — дизайн платформы (аккаунты/конфиги/аватарки), backlog RPC-чата
- docs(todo): Подсистема 3 — Аддоны 2.0 (скрипты/версии) + витрина аддонов + админка
- docs(todo): Подсистема 2 — RPC-чат/друзья/виджеты-телеметрия + рейт-лимиты, доп. правки Подсистемы 3
- chore: переименование combatant-client-26.2 -> mod, добавлены backend/ и frontend/
- docs(backend): implementation plan for accounts-service (Подсистема 1, часть 1)
- chore: пересоздать backend/frontend через cargo new и bun create vite + tailwind; убрать таблицу компонентов из TODO.md
- docs(backend): edition 2024 в плане вместо 2021
- chore: обновить версии до реально актуальных (проверено компиляцией)
- docs: правила платформы (лимит 250 строк, антипаттерны backend/frontend по итогам ресёрча) + frontend/ARCHITECTURE.md
- docs: добавить правило ≤4 файла на папку в правила платформы (backend/frontend)
- docs: commit messages in English from now on
- chore: gitignore .superpowers/ scratch directory
- docs: commit messages in English from now on (mod)
- feat(backend): scaffold accounts-service with health check
- chore(backend): remove target/ build artifacts from git, add gitignore
- chore: track docs/ in git (was accidentally gitignored, never committed)
- chore(backend): convert to Cargo workspace, plan full microservice layout
- feat(backend): accounts/avatars/device_links schema + migration
- feat(backend): argon2id password hashing
This commit is contained in:
loki5512344 2026-09-23 22:38:08 +02:00
parent ec10395511
commit 9d08fa910a
2239 changed files with 35265 additions and 28110 deletions

View file

@ -0,0 +1,91 @@
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.systems.CommandEncoder;
import com.mojang.blaze3d.systems.RenderPass;
import com.mojang.blaze3d.systems.RenderPassDescriptor;
import com.mojang.blaze3d.systems.RenderSystem;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
@Mixin(CommandEncoder.class)
public abstract class CommandEncoderDiagnosticsMixin {
@Unique
private static boolean lovisual$isVulkanBackend() {
try {
if (RenderSystem.getDevice() == null || RenderSystem.getDevice().getDeviceInfo() == null) return false;
String backendName = RenderSystem.getDevice().getDeviceInfo().backendName();
return backendName != null && backendName.toLowerCase(java.util.Locale.ROOT).contains("vulkan");
} catch (Throwable ignored) {
return false;
}
}
@Unique
private static String lovisual$label(RenderPassDescriptor descriptor) {
if (descriptor == null || descriptor.label() == null) return "<null-label>";
try {
String label = descriptor.label().get();
return label == null ? "<null-label>" : label;
} catch (Throwable t) {
return "<label-error:" + t.getClass().getSimpleName() + ">";
}
}
@Inject(method = "createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPass;", at = @At("HEAD"))
private void lovisual$wrapperCreateHead(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPass> cir) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.create.head",
"label", lovisual$label(descriptor),
"colors", descriptor == null || descriptor.colorAttachments() == null ? -1 : descriptor.colorAttachments().size(),
"depth", descriptor != null && descriptor.depthAttachment() != null);
}
@Inject(method = "createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPass;", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/CommandEncoderBackend;createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPassBackend;", shift = At.Shift.BEFORE))
private void lovisual$backendCreateBefore(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPass> cir) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.backend.create.before", "label", lovisual$label(descriptor));
}
@Inject(method = "createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPass;", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/CommandEncoderBackend;createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPassBackend;", shift = At.Shift.AFTER))
private void lovisual$backendCreateAfter(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPass> cir) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.backend.create.after", "label", lovisual$label(descriptor));
}
@Inject(method = "createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPass;", at = @At("RETURN"))
private void lovisual$wrapperCreateReturn(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPass> cir) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.create.return",
"label", lovisual$label(descriptor),
"pass", cir.getReturnValue() == null ? "<null>" : cir.getReturnValue().getClass().getName());
}
@Inject(method = "submitRenderPass", at = @At("HEAD"))
private void lovisual$wrapperSubmitHead(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.submit.head");
}
@Inject(method = "submitRenderPass", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/CommandEncoderBackend;submitRenderPass()V", shift = At.Shift.BEFORE))
private void lovisual$backendSubmitBefore(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.backend.submit.before");
}
@Inject(method = "submitRenderPass", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/CommandEncoderBackend;submitRenderPass()V", shift = At.Shift.AFTER))
private void lovisual$backendSubmitAfter(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.backend.submit.after");
}
@Inject(method = "submitRenderPass", at = @At("TAIL"))
private void lovisual$wrapperSubmitTail(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("commandencoder.wrapper.submit.tail");
}
}

View file

@ -0,0 +1,83 @@
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderPass;
import com.mojang.blaze3d.systems.RenderSystem;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
@Mixin(RenderPass.class)
public abstract class RenderPassWrapperDiagnosticsMixin {
@Unique
private static boolean lovisual$isVulkanBackend() {
try {
if (RenderSystem.getDevice() == null || RenderSystem.getDevice().getDeviceInfo() == null) return false;
String backendName = RenderSystem.getDevice().getDeviceInfo().backendName();
return backendName != null && backendName.toLowerCase(java.util.Locale.ROOT).contains("vulkan");
} catch (Throwable ignored) {
return false;
}
}
@Unique
private static String lovisual$pipelineId(RenderPipeline pipeline) {
if (pipeline == null) return "<null>";
try {
return String.valueOf(pipeline.getLocation());
} catch (Throwable ignored) {
return pipeline.toString();
}
}
@Inject(method = "setPipeline", at = @At("HEAD"))
private void lovisual$setPipelineHead(RenderPipeline pipeline, CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.setpipeline.head", "pipeline", lovisual$pipelineId(pipeline));
}
@Inject(
method = "setPipeline",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderPassBackend;setPipeline(Lcom/mojang/blaze3d/pipeline/RenderPipeline;)V", shift = At.Shift.BEFORE)
)
private void lovisual$backendSetPipelineBefore(RenderPipeline pipeline, CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.backend.setpipeline.before", "pipeline", lovisual$pipelineId(pipeline));
}
@Inject(
method = "setPipeline",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderPassBackend;setPipeline(Lcom/mojang/blaze3d/pipeline/RenderPipeline;)V", shift = At.Shift.AFTER)
)
private void lovisual$backendSetPipelineAfter(RenderPipeline pipeline, CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.backend.setpipeline.after", "pipeline", lovisual$pipelineId(pipeline));
}
@Inject(method = "close", at = @At("HEAD"))
private void lovisual$closeHead(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.close.head");
}
@Inject(method = "close", at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", shift = At.Shift.BEFORE))
private void lovisual$closeOnFinishBefore(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.close.onfinish.before");
}
@Inject(method = "close", at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", shift = At.Shift.AFTER))
private void lovisual$closeOnFinishAfter(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.close.onfinish.after");
}
@Inject(method = "close", at = @At("TAIL"))
private void lovisual$closeTail(CallbackInfo ci) {
if (!lovisual$isVulkanBackend()) return;
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.wrapper.close.tail");
}
}

View file

@ -0,0 +1,16 @@
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.shaders.GpuDebugOptions;
import com.mojang.blaze3d.vulkan.VulkanBackend;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
@Mixin(VulkanBackend.class)
public abstract class VulkanBackendMixin {
@ModifyVariable(method = "createDevice", at = @At("HEAD"), argsOnly = true)
private GpuDebugOptions lovisual$forceDebugOptions(GpuDebugOptions options) {
return VulkanCrashDiagnostics.forceDebugOptions(options);
}
}

View file

@ -0,0 +1,35 @@
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.vulkan.checkpoints.CheckpointExtension;
import org.lwjgl.vulkan.VkCommandBuffer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge;
import java.util.function.Supplier;
@Mixin(targets = "com.mojang.blaze3d.vulkan.checkpoints.AbstractCheckpointStorage")
public abstract class VulkanCheckpointDiagnosticsMixin {
@Inject(method = "recordCheckpoint(Lorg/lwjgl/vulkan/VkCommandBuffer;Lcom/mojang/blaze3d/vulkan/checkpoints/CheckpointExtension$CheckpointType;Ljava/util/function/Supplier;)V", at = @At("HEAD"))
private void lovisual$logCheckpoint(VkCommandBuffer commandBuffer,
CheckpointExtension.CheckpointType type,
Supplier<String> label,
CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet(VulkanRenderStateBridge.forceDisableCheckpoints() ? "checkpoint.disabled" : "checkpoint.record",
"type", type,
"label", safeLabel(label),
"commandBuffer", commandBuffer == null ? 0L : commandBuffer.address());
}
private static String safeLabel(Supplier<String> label) {
if (label == null) return "<null>";
try {
return label.get();
} catch (Throwable t) {
return "<label-error:" + VulkanCrashDiagnostics.throwable(t) + ">";
}
}
}

View file

@ -0,0 +1,94 @@
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.systems.RenderPassBackend;
import com.mojang.blaze3d.systems.RenderPassDescriptor;
import com.mojang.blaze3d.vulkan.VulkanCommandEncoder;
import org.lwjgl.vulkan.VkRenderingAttachmentInfo;
import org.lwjgl.vulkan.VkRenderingInfo;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge;
@Mixin(VulkanCommandEncoder.class)
public abstract class VulkanCommandEncoderDiagnosticsMixin {
@Inject(method = "createRenderPass", at = @At("HEAD"))
private void lovisual$renderPassCreateHead(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPassBackend> cir) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.create.head",
"colors", descriptor == null || descriptor.colorAttachments() == null ? -1 : descriptor.colorAttachments().size(),
"depth", descriptor != null && descriptor.depthAttachment() != null,
"samples", VulkanRenderStateBridge.currentRenderPassSamples(),
"stencil", VulkanRenderStateBridge.currentRenderPassNeedsStencilAttachment());
}
@Inject(
method = "createRenderPass",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRDynamicRendering;vkCmdBeginRenderingKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;Lorg/lwjgl/vulkan/VkRenderingInfo;)V", shift = At.Shift.BEFORE)
)
private void lovisual$beforeBeginRendering(RenderPassDescriptor descriptor,
CallbackInfoReturnable<RenderPassBackend> cir,
@Local VkRenderingInfo renderingInfo) {
int colorResolves = 0;
VkRenderingAttachmentInfo.Buffer colors = renderingInfo == null ? null : renderingInfo.pColorAttachments();
if (colors != null) {
for (int i = 0; i < colors.remaining(); i++) {
if (colors.get(i).resolveImageView() != 0L) colorResolves++;
}
}
VkRenderingAttachmentInfo depth = renderingInfo == null ? null : renderingInfo.pDepthAttachment();
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.beginrendering.before",
"colors", descriptor == null || descriptor.colorAttachments() == null ? -1 : descriptor.colorAttachments().size(),
"depth", descriptor != null && descriptor.depthAttachment() != null,
"samples", VulkanRenderStateBridge.currentRenderPassSamples(),
"stencil", VulkanRenderStateBridge.currentRenderPassNeedsStencilAttachment(),
"colorResolves", colorResolves,
"depthResolveMode", depth == null ? 0 : depth.resolveMode(),
"depthResolveView", depth == null ? 0L : depth.resolveImageView(),
"renderingInfo", renderingInfo == null ? 0L : renderingInfo.address());
}
@Inject(
method = "createRenderPass",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRDynamicRendering;vkCmdBeginRenderingKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;Lorg/lwjgl/vulkan/VkRenderingInfo;)V", shift = At.Shift.AFTER)
)
private void lovisual$afterBeginRendering(RenderPassDescriptor descriptor,
CallbackInfoReturnable<RenderPassBackend> cir) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.beginrendering.after");
}
@Inject(method = "createRenderPass", at = @At("RETURN"))
private void lovisual$renderPassCreated(RenderPassDescriptor descriptor, CallbackInfoReturnable<RenderPassBackend> cir) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.create.return",
"backend", cir.getReturnValue() == null ? "<null>" : cir.getReturnValue().getClass().getName());
}
@Inject(method = "submitRenderPass", at = @At("HEAD"))
private void lovisual$submitVulkanRenderPassHead(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.submit.head");
}
@Inject(
method = "submitRenderPass",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRDynamicRendering;vkCmdEndRenderingKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;)V", shift = At.Shift.BEFORE)
)
private void lovisual$beforeEndRendering(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.endrendering.before");
}
@Inject(
method = "submitRenderPass",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRDynamicRendering;vkCmdEndRenderingKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;)V", shift = At.Shift.AFTER)
)
private void lovisual$afterEndRendering(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.endrendering.after");
}
@Inject(method = "submitRenderPass", at = @At("TAIL"))
private void lovisual$submitVulkanRenderPassTail(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.submit.tail");
}
}

View file

@ -0,0 +1,19 @@
package dev.loki.lovisual.mixins;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
@Mixin(targets = "com.mojang.blaze3d.vulkan.VulkanDebug$Enabled")
public abstract class VulkanDebugMessageMixin {
@Inject(method = "onDebugMessage", at = @At("HEAD"))
private void lovisual$captureDriverDebugMessage(int severity,
int type,
long callbackData,
long userData,
CallbackInfoReturnable<Integer> cir) {
VulkanCrashDiagnostics.driverDebugMessage(severity, type, callbackData);
}
}

View file

@ -0,0 +1,30 @@
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.shaders.ShaderSource;
import com.mojang.blaze3d.vulkan.VulkanDevice;
import com.mojang.blaze3d.vulkan.VulkanInstance;
import com.mojang.blaze3d.vulkan.VulkanPhysicalDevice;
import com.mojang.blaze3d.vulkan.checkpoints.CheckpointExtension;
import org.lwjgl.vulkan.VkDevice;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
import java.util.Set;
@Mixin(VulkanDevice.class)
public abstract class VulkanDeviceDiagnosticsMixin {
@Inject(method = "<init>", at = @At("RETURN"))
private void lovisual$dumpVulkanDevice(ShaderSource shaderSource,
VulkanInstance instance,
VulkanPhysicalDevice physicalDevice,
Set<String> enabledExtensions,
VkDevice device,
long vma,
CheckpointExtension checkpointExtension,
CallbackInfo ci) {
VulkanCrashDiagnostics.dumpDevice(instance, physicalDevice, enabledExtensions);
}
}

View file

@ -0,0 +1,27 @@
package dev.loki.lovisual.mixins;
import org.lwjgl.vulkan.KHRSynchronization2;
import org.lwjgl.vulkan.VkQueue;
import org.lwjgl.vulkan.VkSubmitInfo2;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
@Mixin(targets = "com.mojang.blaze3d.vulkan.VulkanQueue$Submission")
public abstract class VulkanQueueSubmissionMixin {
@Redirect(
method = "close",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRSynchronization2;vkQueueSubmit2KHR(Lorg/lwjgl/vulkan/VkQueue;Lorg/lwjgl/vulkan/VkSubmitInfo2$Buffer;J)I")
)
private int lovisual$diagnoseQueueSubmit(VkQueue queue, VkSubmitInfo2.Buffer submits, long fence) {
int submitCount = submits == null ? -1 : submits.remaining();
VulkanCrashDiagnostics.breadcrumbQuiet("queue.submit.before", "submits", submitCount, "fence", fence);
int result = KHRSynchronization2.vkQueueSubmit2KHR(queue, submits, fence);
VulkanCrashDiagnostics.breadcrumbQuiet("queue.submit.after", "result", result, "submits", submitCount, "fence", fence);
if (result != 0) {
VulkanCrashDiagnostics.breadcrumb("queue.submit.failed", "result", result, "submits", submitCount, "fence", fence);
}
return result;
}
}

View file

@ -0,0 +1,288 @@
package dev.loki.lovisual.mixins;
import com.mojang.blaze3d.IndexType;
import com.mojang.blaze3d.buffers.GpuBuffer;
import com.mojang.blaze3d.buffers.GpuBufferSlice;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.systems.RenderPass;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import com.mojang.blaze3d.vulkan.VulkanCommandEncoder;
import com.mojang.blaze3d.vulkan.VulkanDevice;
import com.mojang.blaze3d.vulkan.VulkanRenderPass;
import com.mojang.blaze3d.vulkan.checkpoints.CheckpointExtension;
import org.lwjgl.PointerBuffer;
import org.lwjgl.vulkan.VkCommandBuffer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
import java.nio.IntBuffer;
import java.util.Collection;
import java.util.function.Supplier;
@Mixin(VulkanRenderPass.class)
public abstract class VulkanRenderPassDiagnosticsMixin {
@Inject(method = "<init>", at = @At("HEAD"))
private static void lovisual$ctorHead(VulkanDevice device,
VulkanCommandEncoder encoder,
VkCommandBuffer commandBuffer,
CheckpointExtension.CheckpointStorage checkpointStorage,
RenderPass.RenderArea renderArea,
int outputWidth,
int outputHeight,
boolean hasDepth,
Supplier<String> label,
CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.object.ctor.head",
"label", labelText(label),
"area", areaText(renderArea),
"output", outputWidth + "x" + outputHeight,
"depth", hasDepth,
"commandBuffer", commandBuffer == null ? 0L : commandBuffer.address());
}
@Inject(method = "<init>", at = @At("TAIL"))
private void lovisual$ctorTail(VulkanDevice device,
VulkanCommandEncoder encoder,
VkCommandBuffer commandBuffer,
CheckpointExtension.CheckpointStorage checkpointStorage,
RenderPass.RenderArea renderArea,
int outputWidth,
int outputHeight,
boolean hasDepth,
Supplier<String> label,
CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.object.ctor.tail",
"label", labelText(label),
"area", areaText(renderArea));
}
@Inject(method = "setPipeline", at = @At("HEAD"))
private void lovisual$setPipelineHead(RenderPipeline pipeline, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.setpipeline.head", "pipeline", pipelineId(pipeline));
}
@Inject(
method = "setPipeline",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vulkan/VulkanDevice;getOrCompilePipeline(Lcom/mojang/blaze3d/pipeline/RenderPipeline;)Lcom/mojang/blaze3d/vulkan/VulkanRenderPipeline;", shift = At.Shift.BEFORE)
)
private void lovisual$compilePipelineBefore(RenderPipeline pipeline, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.pipeline.get.before", "pipeline", pipelineId(pipeline));
}
@Inject(
method = "setPipeline",
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vulkan/VulkanDevice;getOrCompilePipeline(Lcom/mojang/blaze3d/pipeline/RenderPipeline;)Lcom/mojang/blaze3d/vulkan/VulkanRenderPipeline;", shift = At.Shift.AFTER)
)
private void lovisual$compilePipelineAfter(RenderPipeline pipeline, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.pipeline.get.after", "pipeline", pipelineId(pipeline));
}
@Inject(
method = "setPipeline",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCmdBindPipeline(Lorg/lwjgl/vulkan/VkCommandBuffer;IJ)V", shift = At.Shift.BEFORE)
)
private void lovisual$bindPipelineBefore(RenderPipeline pipeline, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.bindpipeline.before", "pipeline", pipelineId(pipeline));
}
@Inject(
method = "setPipeline",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCmdBindPipeline(Lorg/lwjgl/vulkan/VkCommandBuffer;IJ)V", shift = At.Shift.AFTER)
)
private void lovisual$bindPipelineAfter(RenderPipeline pipeline, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.bindpipeline.after", "pipeline", pipelineId(pipeline));
}
@Inject(method = "setPipeline", at = @At("TAIL"))
private void lovisual$setPipelineTail(RenderPipeline pipeline, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.setpipeline.tail", "pipeline", pipelineId(pipeline));
}
@Inject(method = "bindTexture", at = @At("HEAD"))
private void lovisual$bindTextureHead(String name, GpuTextureView view, GpuSampler sampler, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.bindtexture.head", "name", name, "view", objectText(view), "sampler", objectText(sampler));
}
@Inject(method = "setUniform(Ljava/lang/String;Lcom/mojang/blaze3d/buffers/GpuBufferSlice;)V", at = @At("HEAD"))
private void lovisual$setUniformSliceHead(String name, GpuBufferSlice slice, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.uniform.slice.head", "name", name, "slice", sliceText(slice));
}
@Inject(method = "setUniform(Ljava/lang/String;Lcom/mojang/blaze3d/buffers/GpuBuffer;)V", at = @At("HEAD"))
private void lovisual$setUniformBufferHead(String name, GpuBuffer buffer, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.uniform.buffer.head", "name", name, "buffer", bufferText(buffer));
}
@Inject(method = "enableScissor", at = @At("HEAD"))
private void lovisual$enableScissorHead(int x, int y, int width, int height, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.scissor.enable.head", "rect", x + "," + y + " " + width + "x" + height);
}
@Inject(method = "setVertexBuffer", at = @At("HEAD"))
private void lovisual$vertexBufferHead(int slot, GpuBufferSlice slice, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.vertexbuffer.head", "slot", slot, "slice", sliceText(slice));
}
@Inject(method = "setIndexBuffer", at = @At("HEAD"))
private void lovisual$indexBufferHead(GpuBuffer buffer, IndexType indexType, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.indexbuffer.head", "buffer", bufferText(buffer), "type", indexType);
}
@Inject(method = "pushDescriptors", at = @At("HEAD"))
private void lovisual$pushDescriptorsHead(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.pushdescriptors.head");
}
@Inject(
method = "pushDescriptors",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRPushDescriptor;vkCmdPushDescriptorSetKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;IJILorg/lwjgl/vulkan/VkWriteDescriptorSet$Buffer;)V", shift = At.Shift.BEFORE)
)
private void lovisual$pushDescriptorsNativeBefore(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.pushdescriptors.native.before");
}
@Inject(
method = "pushDescriptors",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/KHRPushDescriptor;vkCmdPushDescriptorSetKHR(Lorg/lwjgl/vulkan/VkCommandBuffer;IJILorg/lwjgl/vulkan/VkWriteDescriptorSet$Buffer;)V", shift = At.Shift.AFTER)
)
private void lovisual$pushDescriptorsNativeAfter(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.pushdescriptors.native.after");
}
@Inject(method = "pushDescriptors", at = @At("TAIL"))
private void lovisual$pushDescriptorsTail(CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.pushdescriptors.tail");
}
@Inject(method = "drawIndexed", at = @At("HEAD"))
private void lovisual$drawIndexedHead(int indexCount, int instanceCount, int firstIndex, int vertexOffset, int firstInstance, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.drawindexed.head",
"indices", indexCount,
"instances", instanceCount,
"firstIndex", firstIndex,
"vertexOffset", vertexOffset,
"firstInstance", firstInstance);
}
@Inject(
method = "drawIndexed",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCmdDrawIndexed(Lorg/lwjgl/vulkan/VkCommandBuffer;IIIII)V", shift = At.Shift.BEFORE)
)
private void lovisual$drawIndexedNativeBefore(int indexCount, int instanceCount, int firstIndex, int vertexOffset, int firstInstance, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.drawindexed.native.before", "indices", indexCount);
}
@Inject(
method = "drawIndexed",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCmdDrawIndexed(Lorg/lwjgl/vulkan/VkCommandBuffer;IIIII)V", shift = At.Shift.AFTER)
)
private void lovisual$drawIndexedNativeAfter(int indexCount, int instanceCount, int firstIndex, int vertexOffset, int firstInstance, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.drawindexed.native.after", "indices", indexCount);
}
@Inject(method = "draw", at = @At("HEAD"))
private void lovisual$drawHead(int vertexCount, int instanceCount, int firstVertex, int firstInstance, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.draw.head",
"vertices", vertexCount,
"instances", instanceCount,
"firstVertex", firstVertex,
"firstInstance", firstInstance);
}
@Inject(
method = "draw",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCmdDraw(Lorg/lwjgl/vulkan/VkCommandBuffer;IIII)V", shift = At.Shift.BEFORE)
)
private void lovisual$drawNativeBefore(int vertexCount, int instanceCount, int firstVertex, int firstInstance, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.draw.native.before", "vertices", vertexCount);
}
@Inject(
method = "draw",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCmdDraw(Lorg/lwjgl/vulkan/VkCommandBuffer;IIII)V", shift = At.Shift.AFTER)
)
private void lovisual$drawNativeAfter(int vertexCount, int instanceCount, int firstVertex, int firstInstance, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.draw.native.after", "vertices", vertexCount);
}
@Inject(method = "multiDraw(Ljava/nio/IntBuffer;III)V", at = @At("HEAD"))
private void lovisual$multiDrawHead(IntBuffer buffer, int firstInstance, int drawCount, int stride, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.multidraw.head", "draws", drawCount, "stride", stride, "remaining", buffer == null ? -1 : buffer.remaining());
}
@Inject(method = "multiDrawIndexed(Ljava/nio/IntBuffer;III)V", at = @At("HEAD"))
private void lovisual$multiDrawIndexedHead(IntBuffer buffer, int firstInstance, int drawCount, int stride, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.multidrawindexed.head", "draws", drawCount, "stride", stride, "remaining", buffer == null ? -1 : buffer.remaining());
}
@Inject(method = "multiDrawIndexed(Lorg/lwjgl/PointerBuffer;Ljava/nio/IntBuffer;Ljava/nio/IntBuffer;I)V", at = @At("HEAD"))
private void lovisual$multiDrawIndexedPointerHead(PointerBuffer firstIndex, IntBuffer indexCount, IntBuffer baseVertex, int drawCount, CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbDraw("renderpass.multidrawindexed.pointer.head", "draws", drawCount);
}
@Inject(method = "drawMultipleIndexed", at = @At("HEAD"))
private <T> void lovisual$drawMultipleIndexedHead(Collection<RenderPass.Draw<T>> draws,
GpuBuffer indexBuffer,
IndexType indexType,
Collection<String> dynamicUniforms,
T extraData,
CallbackInfo ci) {
VulkanCrashDiagnostics.breadcrumbQuiet("renderpass.drawmultipleindexed.head",
"draws", draws == null ? -1 : draws.size(),
"indexBuffer", bufferText(indexBuffer),
"indexType", indexType,
"dynamicUniforms", dynamicUniforms == null ? -1 : dynamicUniforms.size());
}
private static String pipelineId(RenderPipeline pipeline) {
if (pipeline == null) return "<null>";
try {
return String.valueOf(pipeline.getLocation());
} catch (Throwable ignored) {
return pipeline.toString();
}
}
private static String labelText(Supplier<String> label) {
if (label == null) return "<null>";
try {
String value = label.get();
return value == null ? "<null>" : value;
} catch (Throwable t) {
return "<label-error:" + t.getClass().getSimpleName() + ">";
}
}
private static String areaText(RenderPass.RenderArea area) {
if (area == null) return "<null>";
return area.x() + "," + area.y() + " " + area.width() + "x" + area.height();
}
private static String sliceText(GpuBufferSlice slice) {
if (slice == null) return "<null>";
try {
return "offset=" + slice.offset() + " buffer=" + bufferText(slice.buffer());
} catch (Throwable t) {
return "<slice-error:" + t.getClass().getSimpleName() + ">";
}
}
private static String bufferText(GpuBuffer buffer) {
if (buffer == null) return "<null>";
try {
return buffer.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(buffer)) + " closed=" + buffer.isClosed();
} catch (Throwable t) {
return buffer.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(buffer)) + " error=" + t.getClass().getSimpleName();
}
}
private static String objectText(Object object) {
if (object == null) return "<null>";
return object.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(object));
}
}

View file

@ -0,0 +1,37 @@
package dev.loki.lovisual.mixins;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.vulkan.VulkanBindGroupLayout;
import com.mojang.blaze3d.vulkan.VulkanDevice;
import com.mojang.blaze3d.vulkan.VulkanRenderPipeline;
import org.lwjgl.vulkan.VkPipelineDepthStencilStateCreateInfo;
import org.lwjgl.vulkan.VkPipelineRenderingCreateInfoKHR;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug.VulkanCrashDiagnostics;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRenderStateBridge;
@Mixin(VulkanRenderPipeline.class)
public abstract class VulkanRenderPipelineDiagnosticsMixin {
@Inject(
method = "compile",
at = @At(value = "INVOKE", target = "Lorg/lwjgl/vulkan/VK12;vkCreateGraphicsPipelines(Lorg/lwjgl/vulkan/VkDevice;JLorg/lwjgl/vulkan/VkGraphicsPipelineCreateInfo$Buffer;Lorg/lwjgl/vulkan/VkAllocationCallbacks;Ljava/nio/LongBuffer;)I", ordinal = 0, shift = At.Shift.BEFORE)
)
private static void lovisual$logPipelineCompile(VulkanDevice device,
VulkanBindGroupLayout layout,
RenderPipeline pipeline,
long vertexModule,
long fragmentModule,
CallbackInfoReturnable<VulkanRenderPipeline> cir,
@Local VkPipelineRenderingCreateInfoKHR rendering,
@Local VkPipelineDepthStencilStateCreateInfo depthStencil) {
VulkanCrashDiagnostics.breadcrumbQuiet("pipeline.compile.before",
"pipeline", pipeline == null || pipeline.getLocation() == null ? "<unknown>" : pipeline.getLocation(),
"samples", VulkanRenderStateBridge.currentVkSampleCount(),
"stencil", VulkanRenderStateBridge.pipelineNeedsStencilFormat(pipeline),
"stencilMode", VulkanRenderStateBridge.stencilMode());
}
}

View file

@ -0,0 +1,326 @@
package dev.loki.lovisual.render.engine.rhi.backend.vulkan.debug;
import com.mojang.blaze3d.shaders.GpuDebugOptions;
import com.mojang.blaze3d.vulkan.VulkanInstance;
import com.mojang.blaze3d.vulkan.VulkanPhysicalDevice;
import net.minecraft.client.Minecraft;
import org.lwjgl.vulkan.VK12;
import org.lwjgl.vulkan.VkDebugUtilsMessengerCallbackDataEXT;
import org.lwjgl.vulkan.VkPhysicalDeviceLimits;
import org.lwjgl.vulkan.VkPhysicalDeviceProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.loki.lovisual.render.engine.rhi.backend.vulkan.util.VulkanRuntimeGuards;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.Collection;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* Opt-in Vulkan diagnostics. Compiled only when Gradle property lovisualVulkanDebug=true is used.
*/
public enum VulkanCrashDiagnostics {
;
public static final boolean FORCE_DRIVER_DEBUG = bool("lovisual.vulkan.debug.forceDriverDebug", true);
public static final boolean VERBOSE_DRAW_EVENTS = bool("lovisual.vulkan.debug.verboseDraw", false);
public static final long MAX_FILE_BYTES = longProp("lovisual.vulkan.debug.maxBytes", 8L * 1024L * 1024L);
public static final int MAX_EVENT_LINES = intProp("lovisual.vulkan.debug.maxEventLines", 256);
private static final Logger LOGGER = LoggerFactory.getLogger("LoVisual/VulkanDiag");
private static final String DIAGNOSTIC_FILE_NAME = "lovisual-vulkan-diagnostics.log";
private static final Object FILE_LOCK = new Object();
private static final ConcurrentHashMap<String, AtomicInteger> EVENT_COUNTS = new ConcurrentHashMap<>();
private static final AtomicLong WRITTEN_BYTES = new AtomicLong();
private static volatile Path resolvedDiagnosticLog;
private static volatile boolean headerWritten;
private static volatile boolean truncated;
public static GpuDebugOptions forceDebugOptions(GpuDebugOptions options) {
if (!FORCE_DRIVER_DEBUG) return options;
int logLevel = options == null ? 4 : Math.max(options.logLevel(), 4);
boolean labelsOriginally = options != null && options.useLabels();
boolean validationOriginally = options != null && options.useValidationLayers();
boolean labels = labelsOriginally && !VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_DEBUG_LABELS;
boolean validation = true;
GpuDebugOptions forced = new GpuDebugOptions(logLevel, true, labels, validation);
breadcrumb("debug-options.forced",
"original", options,
"forced", forced,
"labelsOriginally", labelsOriginally,
"labelsForcedOff", VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_DEBUG_LABELS,
"validationOriginally", validationOriginally,
"validationEnabled", validation);
LOGGER.warn("[LoVisual/VulkanDiag] forcing Vulkan debug utils + validation layers: {}", forced);
return forced;
}
public static void dumpDevice(VulkanInstance instance,
VulkanPhysicalDevice physicalDevice,
Set<String> requestedDeviceExtensions) {
if (physicalDevice == null) {
breadcrumb("device.dump.skipped", "reason", "physicalDevice=null");
return;
}
try {
VkPhysicalDeviceProperties props = physicalDevice.vkPhysicalDeviceProperties();
VkPhysicalDeviceLimits limits = props == null ? null : props.limits();
String apiVersion = props == null ? "unknown" : version(props.apiVersion());
String driverVersion = props == null ? "unknown" : version(props.driverVersion());
String instanceExtensions = instance == null ? "<no-instance>" : join(instance.getEnabledExtensions());
String deviceExtensions = join(requestedDeviceExtensions);
String sampleCounts = limits == null ? "unknown" : String.format(
"color=0x%X depth=0x%X stencil=0x%X noAttachments=0x%X",
limits.framebufferColorSampleCounts(),
limits.framebufferDepthSampleCounts(),
limits.framebufferStencilSampleCounts(),
limits.framebufferNoAttachmentsSampleCounts()
);
String queues = String.format("graphics=%s compute=%s transfer=%s",
physicalDevice.graphicsQueueFamilyAndIndex(),
physicalDevice.computeQueueFamilyAndIndex(),
physicalDevice.transferQueueFamilyAndIndex());
breadcrumb("device.dump",
"device", physicalDevice.deviceName(),
"vendor", physicalDevice.vendorName(),
"type", physicalDevice.deviceType(),
"driverInfo", physicalDevice.driverInfo(),
"apiVersion", apiVersion,
"driverVersionRaw", driverVersion,
"vkVendorId", props == null ? "unknown" : props.vendorID(),
"vkDeviceId", props == null ? "unknown" : props.deviceID(),
"debugUtilsEnabled", instance != null && instance.debug() != null && instance.debug().enabled(),
"instanceExtensions", instanceExtensions,
"deviceExtensions", deviceExtensions,
"sampleCounts", sampleCounts,
"queues", queues,
"stencil", "runtime-failsafe-only",
"msaa", "config-and-capability-controlled",
"hardDisableLabels", VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_DEBUG_LABELS,
"hardDisableCheckpoints", VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_CHECKPOINTS,
"maxBytes", MAX_FILE_BYTES,
"verboseDraw", VERBOSE_DRAW_EVENTS,
"maxEventLines", MAX_EVENT_LINES);
} catch (Throwable t) {
breadcrumb("device.dump.failed", "error", throwable(t));
}
}
public static void driverDebugMessage(int severity, int type, long callbackData) {
if (callbackData == 0L) {
breadcrumbQuiet("driver.debug.message", "severity", severityName(severity), "type", typeName(type), "message", "<null-callback-data>");
return;
}
try {
VkDebugUtilsMessengerCallbackDataEXT data = VkDebugUtilsMessengerCallbackDataEXT.create(callbackData);
breadcrumbQuiet("driver.debug.message",
"severity", severityName(severity),
"type", typeName(type),
"id", data.pMessageIdNameString(),
"number", data.messageIdNumber(),
"message", sanitize(data.pMessageString()));
} catch (Throwable t) {
breadcrumbQuiet("driver.debug.message.failed",
"severity", severity,
"type", type,
"error", throwable(t));
}
}
public static void breadcrumb(String event, Object... keyValues) {
String line = formatLine(event, keyValues);
LOGGER.warn(line);
writeLine(event, line);
}
public static void breadcrumbQuiet(String event, Object... keyValues) {
if (isNoisyRenderEvent(event) && !VERBOSE_DRAW_EVENTS) return;
writeLine(event, formatLine(event, keyValues));
}
public static void breadcrumbDraw(String event, Object... keyValues) {
if (!VERBOSE_DRAW_EVENTS) return;
writeLine(event, formatLine(event, keyValues));
}
public static String throwable(Throwable t) {
if (t == null) return "null";
return t.getClass().getName() + ": " + String.valueOf(t.getMessage());
}
private static boolean isNoisyRenderEvent(String event) {
if (event == null) return false;
return event.startsWith("renderpass.draw")
|| event.startsWith("renderpass.multidraw")
|| event.startsWith("renderpass.bindtexture")
|| event.startsWith("renderpass.uniform")
|| event.startsWith("renderpass.vertexbuffer")
|| event.startsWith("renderpass.indexbuffer")
|| event.startsWith("renderpass.scissor")
|| event.startsWith("renderpass.pushdescriptors");
}
private static boolean allowEvent(String event) {
if (event == null || MAX_EVENT_LINES <= 0) return true;
AtomicInteger counter = EVENT_COUNTS.computeIfAbsent(event, ignored -> new AtomicInteger());
int count = counter.incrementAndGet();
if (count <= MAX_EVENT_LINES) return true;
if (count == MAX_EVENT_LINES + 1) {
writeLineDirect(formatLine("event.suppressed", "event", event, "limit", MAX_EVENT_LINES));
}
return false;
}
private static String formatLine(String event, Object... keyValues) {
StringBuilder sb = new StringBuilder(256);
sb.append('[').append(Instant.now()).append("] ").append(event == null ? "<null-event>" : event);
if (keyValues != null) {
for (int i = 0; i < keyValues.length; i += 2) {
Object key = keyValues[i];
Object value = i + 1 < keyValues.length ? keyValues[i + 1] : "<missing>";
sb.append(' ').append(String.valueOf(key)).append('=').append(sanitize(String.valueOf(value)));
}
}
return sb.toString();
}
private static String sanitize(String value) {
if (value == null) return "null";
return value.replace('\r', ' ').replace('\n', ' ').replace('\t', ' ');
}
private static String join(Collection<String> values) {
if (values == null || values.isEmpty()) return "<empty>";
StringJoiner joiner = new StringJoiner(",");
for (String value : values) joiner.add(value);
return joiner.toString();
}
private static String version(int version) {
return VK12.VK_VERSION_MAJOR(version) + "." + VK12.VK_VERSION_MINOR(version) + "." + VK12.VK_VERSION_PATCH(version)
+ " (0x" + Integer.toHexString(version) + ")";
}
private static String severityName(int severity) {
if ((severity & 0x1000) != 0) return "ERROR(0x" + Integer.toHexString(severity) + ")";
if ((severity & 0x0100) != 0) return "WARNING(0x" + Integer.toHexString(severity) + ")";
if ((severity & 0x0010) != 0) return "INFO(0x" + Integer.toHexString(severity) + ")";
if ((severity & 0x0001) != 0) return "VERBOSE(0x" + Integer.toHexString(severity) + ")";
return "UNKNOWN(0x" + Integer.toHexString(severity) + ")";
}
private static String typeName(int type) {
StringJoiner joiner = new StringJoiner("|");
if ((type & 0x1) != 0) joiner.add("GENERAL");
if ((type & 0x2) != 0) joiner.add("VALIDATION");
if ((type & 0x4) != 0) joiner.add("PERFORMANCE");
String joined = joiner.toString();
return joined.isEmpty() ? "UNKNOWN(0x" + Integer.toHexString(type) + ")" : joined + "(0x" + Integer.toHexString(type) + ")";
}
private static Path diagnosticLogPath() {
Path cached = resolvedDiagnosticLog;
if (cached != null) return cached;
Path resolved = resolveMinecraftLogsPath();
resolvedDiagnosticLog = resolved;
return resolved;
}
private static Path resolveMinecraftLogsPath() {
try {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft != null && minecraft.gameDirectory != null) {
return minecraft.gameDirectory.toPath().resolve("logs").resolve(DIAGNOSTIC_FILE_NAME);
}
} catch (Throwable ignored) {
// Vulkan init can run early enough that Minecraft may not be fully accessible.
}
return Path.of("logs", DIAGNOSTIC_FILE_NAME);
}
private static void writeLine(String event, String line) {
if (!allowEvent(event)) return;
writeLineDirect(line);
}
private static void writeLineDirect(String line) {
synchronized (FILE_LOCK) {
try {
Path log = diagnosticLogPath();
Path parent = log.getParent();
if (parent != null) Files.createDirectories(parent);
if (!headerWritten) {
String header = "=== LoVisual Vulkan diagnostics ===\n" +
"modId=simplefullbright\n" +
"entrypoint=dev.loki.lovisual.LoVisual\n" +
"path=" + log.toAbsolutePath() + '\n' +
"stencil=runtime-failsafe-only" + '\n' +
"msaa=config-and-capability-controlled" + '\n' +
"hardDisableLabels=" + VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_DEBUG_LABELS + '\n' +
"hardDisableCheckpoints=" + VulkanRuntimeGuards.FORCE_DISABLE_VULKAN_CHECKPOINTS + '\n' +
"maxBytes=" + MAX_FILE_BYTES + '\n' +
"maxEventLines=" + MAX_EVENT_LINES + '\n' +
"verboseDraw=" + VERBOSE_DRAW_EVENTS + '\n';
Files.writeString(log, header,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING,
StandardOpenOption.WRITE);
WRITTEN_BYTES.set(header.getBytes(StandardCharsets.UTF_8).length);
headerWritten = true;
}
if (MAX_FILE_BYTES > 0 && WRITTEN_BYTES.get() >= MAX_FILE_BYTES) {
if (!truncated) {
String marker = formatLine("file.truncated", "maxBytes", MAX_FILE_BYTES) + System.lineSeparator();
Files.writeString(log, marker, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE);
WRITTEN_BYTES.addAndGet(marker.getBytes(StandardCharsets.UTF_8).length);
truncated = true;
}
return;
}
String out = line + System.lineSeparator();
Files.writeString(log, out,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND,
StandardOpenOption.WRITE);
WRITTEN_BYTES.addAndGet(out.getBytes(StandardCharsets.UTF_8).length);
} catch (IOException ignored) {
// The normal Minecraft log still receives LOGGER output for explicit breadcrumb(...).
}
}
}
private static boolean bool(String key, boolean fallback) {
String value = System.getProperty(key);
if (value == null || value.isBlank()) return fallback;
return Boolean.parseBoolean(value);
}
private static int intProp(String key, int fallback) {
String value = System.getProperty(key);
if (value == null || value.isBlank()) return fallback;
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException ignored) {
return fallback;
}
}
private static long longProp(String key, long fallback) {
String value = System.getProperty(key);
if (value == null || value.isBlank()) return fallback;
try {
return Long.parseLong(value.trim());
} catch (NumberFormatException ignored) {
return fallback;
}
}
}

View file

@ -0,0 +1,22 @@
{
"required": true,
"plugin": "dev.loki.lovisual.mixins.LoVisualMixinPlugin",
"minVersion": "0.8",
"package": "dev.loki.lovisual.mixins",
"compatibilityLevel": "JAVA_21",
"client": [
"CommandEncoderDiagnosticsMixin",
"RenderPassWrapperDiagnosticsMixin",
"VulkanBackendMixin",
"VulkanCheckpointDiagnosticsMixin",
"VulkanCommandEncoderDiagnosticsMixin",
"VulkanDebugMessageMixin",
"VulkanDeviceDiagnosticsMixin",
"VulkanQueueSubmissionMixin",
"VulkanRenderPassDiagnosticsMixin",
"VulkanRenderPipelineDiagnosticsMixin"
],
"injectors": {
"defaultRequire": 1
}
}