From 9ca5e2052e0c3d4d3e1f7483553f6156a5b17044 Mon Sep 17 00:00:00 2001 From: xirreal Date: Sat, 14 Feb 2026 00:50:31 +0100 Subject: [PATCH 1/2] Actually make it work properly. 2.0 was a mess --- README.md | 2 +- gradle.properties | 6 +- settings.gradle | 2 +- src/main/java/dev/xirreal/DebuggerPicker.java | 373 ++++++++++++++++++ src/main/java/dev/xirreal/GfxDebuggers.java | 304 ++++++-------- src/main/java/dev/xirreal/PlatformUtils.java | 200 +++------- .../assets/gfx-debuggers/frame-debugger.png | Bin 0 -> 2303 bytes .../assets/gfx-debuggers/gpu-trace.png | Bin 0 -> 2285 bytes .../assets/gfx-debuggers/renderdoc.png | Bin 0 -> 941 bytes src/main/resources/fabric.mod.json | 3 +- 10 files changed, 540 insertions(+), 350 deletions(-) create mode 100644 src/main/java/dev/xirreal/DebuggerPicker.java create mode 100644 src/main/resources/assets/gfx-debuggers/frame-debugger.png create mode 100644 src/main/resources/assets/gfx-debuggers/gpu-trace.png create mode 100644 src/main/resources/assets/gfx-debuggers/renderdoc.png diff --git a/README.md b/README.md index 693557e..be01bec 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This version-agnostic Fabric mod allows you to inject NSight or Renderdoc into Minecraft, without having to fiddle around with launchers, args or environment variables. At launch, it will ask you which debugger you want to attach, and injection can be skipped by closing the dialog. Supports both Linux and Windows, and should work with any version of Minecraft[citation needed], as long as Fabric was ported to it. -Officially supports 1.18.2+, with loader versions 0.15.0+. Compiled with JDK 17, so it may need that or newer to run but I didn't test mostly cause I'm lazy. +Requires Fabric Loader 0.14.0+ and Java 17+. ### Usage diff --git a/gradle.properties b/gradle.properties index 87c778d..de80036 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,9 +8,9 @@ org.gradle.configuration-cache=false # Fabric Properties # check these on https://fabricmc.net/develop minecraft_version=1.18.2 -loader_version=0.15.0 -loom_version=1.15-SNAPSHOT +loader_version=0.14.0 +loom_version=1.14-SNAPSHOT -mod_version=2.0.0 +mod_version=3.0.0 maven_group=dev.xirreal archives_base_name=gfx-debuggers diff --git a/settings.gradle b/settings.gradle index 75c4d72..dfacbd1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,4 +7,4 @@ pluginManagement { mavenCentral() gradlePluginPortal() } -} \ No newline at end of file +} diff --git a/src/main/java/dev/xirreal/DebuggerPicker.java b/src/main/java/dev/xirreal/DebuggerPicker.java new file mode 100644 index 0000000..175e076 --- /dev/null +++ b/src/main/java/dev/xirreal/DebuggerPicker.java @@ -0,0 +1,373 @@ +package dev.xirreal; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.awt.image.*; +import java.util.concurrent.*; +import javax.imageio.*; +import javax.swing.*; +import javax.swing.border.*; + +public class DebuggerPicker extends JFrame { + + private final CountDownLatch latch = new CountDownLatch(1); + + public static enum DebuggerSelection { + GPU_TRACE, + FRAME_DEBUGGER, + RENDERDOC, + NONE, + } + + private DebuggerSelection selectedDebugger = DebuggerSelection.NONE; + + private static final Color BG_PRIMARY = new Color(17, 17, 21); + private static final Color BG_SURFACE = new Color(28, 28, 35); + private static final Color BG_HOVER = new Color(40, 40, 50); + private static final Color BORDER_COLOR = new Color(55, 55, 70); + private static final Color TEXT_PRIMARY = new Color(237, 237, 242); + private static final Color TEXT_SECONDARY = new Color(145, 145, 165); + private static final Color ACCENT_BLUE = new Color(96, 165, 250); + private static final Color ACCENT_GREEN = new Color(74, 222, 128); + private static final Color ACCENT_ORANGE = new Color(251, 146, 60); + + private static final Font FONT_BODY = new Font(Font.SANS_SERIF, Font.PLAIN, 13); + private static final Font FONT_BUTTON = new Font(Font.SANS_SERIF, Font.BOLD, 13); + + public DebuggerPicker() { + setTitle("Graphics Debugger Selector"); + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setResizable(false); + try { + BufferedImage rawIcon = ImageIO.read(DebuggerPicker.class.getResourceAsStream("/assets/gfx-debuggers/icon.png")); + int size = rawIcon.getWidth(); + BufferedImage roundedIcon = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D ig = roundedIcon.createGraphics(); + ig.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + ig.setColor(Color.WHITE); + ig.fill(new RoundRectangle2D.Float(0, 0, size, size, size * 0.25f, size * 0.25f)); + ig.setComposite(AlphaComposite.SrcIn); + ig.drawImage(rawIcon, 0, 0, null); + ig.dispose(); + setIconImage(roundedIcon); + } catch (Exception ignored) {} + addWindowListener( + new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + latch.countDown(); + } + } + ); + + JPanel root = new JPanel(new BorderLayout()) { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(BG_PRIMARY); + g2.fill(new Rectangle2D.Float(0, 0, getWidth(), getHeight())); + g2.dispose(); + } + }; + root.setOpaque(false); + root.setBorder(BorderFactory.createEmptyBorder(16, 24, 16, 24)); + + JPanel header = new JPanel(); + header.setLayout(new BoxLayout(header, BoxLayout.Y_AXIS)); + header.setOpaque(false); + header.setBorder(BorderFactory.createEmptyBorder(0, 0, 18, 0)); + + JLabel subtitle = new JLabel("Select a debugger to attach to this session"); + subtitle.setFont(FONT_BODY); + subtitle.setForeground(TEXT_SECONDARY); + JPanel subtitleCenter = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); + subtitleCenter.setOpaque(false); + subtitleCenter.add(subtitle); + header.add(subtitleCenter); + + root.add(header, BorderLayout.NORTH); + + JPanel cards = new JPanel(); + cards.setLayout(new BoxLayout(cards, BoxLayout.Y_AXIS)); + cards.setOpaque(false); + + cards.add( + createDebuggerCard( + "NSight GPU Trace Profiler", + "Record and analyze GPU frame timings and performance metrics", + ACCENT_ORANGE, + DebuggerSelection.GPU_TRACE, + createImageIcon("/assets/gfx-debuggers/gpu-trace.png") + ) + ); + cards.add(Box.createVerticalStrut(8)); + cards.add( + createDebuggerCard( + "NSight Frame Debugger", + "Capture and inspect individual rendered frames", + ACCENT_BLUE, + DebuggerSelection.FRAME_DEBUGGER, + createImageIcon("/assets/gfx-debuggers/frame-debugger.png") + ) + ); + cards.add(Box.createVerticalStrut(8)); + cards.add( + createDebuggerCard( + "RenderDoc", + "Open-source and cross-vendor graphics debugging tool", + ACCENT_GREEN, + DebuggerSelection.RENDERDOC, + createImageIcon("/assets/gfx-debuggers/renderdoc.png") + ) + ); + + cards.add(Box.createVerticalStrut(16)); + + JSeparator sep = new JSeparator(SwingConstants.HORIZONTAL) { + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setColor(BORDER_COLOR); + g2.fillRect(0, getHeight() / 2, getWidth(), 1); + g2.dispose(); + } + }; + sep.setMaximumSize(new Dimension(Integer.MAX_VALUE, 1)); + sep.setPreferredSize(new Dimension(0, 1)); + sep.setAlignmentX(Component.LEFT_ALIGNMENT); + cards.add(sep); + cards.add(Box.createVerticalStrut(16)); + + JPanel skipBtn = createSkipButton(); + cards.add(skipBtn); + + root.add(cards, BorderLayout.CENTER); + setContentPane(root); + + final Point[] dragOffset = { null }; + root.addMouseListener( + new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + dragOffset[0] = e.getPoint(); + } + } + ); + root.addMouseMotionListener( + new MouseMotionAdapter() { + @Override + public void mouseDragged(MouseEvent e) { + if (dragOffset[0] != null) { + Point loc = getLocation(); + setLocation(loc.x + e.getX() - dragOffset[0].x, loc.y + e.getY() - dragOffset[0].y); + } + } + } + ); + + pack(); + setLocationRelativeTo(null); + } + + private JPanel createDebuggerCard(String name, String description, Color accent, DebuggerSelection value, Icon icon) { + JPanel card = new JPanel(new BorderLayout(12, 0)) { + private boolean hovered = false; + + { + setOpaque(false); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + setBorder(BorderFactory.createEmptyBorder(12, 14, 12, 14)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 72)); + + addMouseListener( + new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + hovered = true; + repaint(); + } + + @Override + public void mouseExited(MouseEvent e) { + hovered = false; + repaint(); + } + + @Override + public void mouseClicked(MouseEvent e) { + selectedDebugger = value; + dispose(); + } + } + ); + } + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Color bg = hovered ? BG_HOVER : BG_SURFACE; + g2.setColor(bg); + g2.fill(new RoundRectangle2D.Float(0, 0, getWidth(), getHeight(), 12, 12)); + Color border = hovered ? accent.darker() : BORDER_COLOR; + g2.setColor(border); + g2.setStroke(new BasicStroke(1f)); + g2.draw(new RoundRectangle2D.Float(0.5f, 0.5f, getWidth() - 1, getHeight() - 1, 12, 12)); + g2.dispose(); + } + }; + + JLabel iconLabel = new JLabel(icon); + iconLabel.setVerticalAlignment(SwingConstants.CENTER); + card.add(iconLabel, BorderLayout.WEST); + + JPanel textPanel = new JPanel(); + textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.Y_AXIS)); + textPanel.setOpaque(false); + + JLabel nameLabel = new JLabel(name); + nameLabel.setFont(FONT_BUTTON); + nameLabel.setForeground(TEXT_PRIMARY); + nameLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + textPanel.add(nameLabel); + textPanel.add(Box.createVerticalStrut(2)); + + JLabel descLabel = new JLabel(description); + descLabel.setFont(FONT_BODY); + descLabel.setForeground(TEXT_SECONDARY); + descLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + textPanel.add(descLabel); + + card.add(textPanel, BorderLayout.CENTER); + + JPanel arrow = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.setColor(TEXT_SECONDARY); + g2.setStroke(new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + int midY = getHeight() / 2; + int midX = getWidth() / 2; + g2.drawLine(midX - 2, midY - 5, midX + 1, midY); + g2.drawLine(midX + 1, midY, midX - 2, midY + 5); + g2.dispose(); + } + }; + arrow.setOpaque(false); + arrow.setPreferredSize(new Dimension(16, 16)); + JPanel arrowWrapper = new JPanel(new BorderLayout()); + arrowWrapper.setOpaque(false); + arrowWrapper.setBorder(BorderFactory.createEmptyBorder(0, 16, 0, 0)); + arrowWrapper.add(arrow, BorderLayout.CENTER); + card.add(arrowWrapper, BorderLayout.EAST); + + card.setAlignmentX(Component.LEFT_ALIGNMENT); + return card; + } + + private JPanel createSkipButton() { + JPanel btn = new JPanel(new BorderLayout()) { + private boolean hovered = false; + + { + setOpaque(false); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + setBorder(BorderFactory.createEmptyBorder(10, 14, 10, 14)); + setMaximumSize(new Dimension(Integer.MAX_VALUE, 44)); + + addMouseListener( + new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + hovered = true; + repaint(); + } + + @Override + public void mouseExited(MouseEvent e) { + hovered = false; + repaint(); + } + + @Override + public void mouseClicked(MouseEvent e) { + selectedDebugger = DebuggerSelection.NONE; + dispose(); + } + } + ); + } + + @Override + protected void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Color bg = hovered ? BG_HOVER : BG_SURFACE; + g2.setColor(bg); + g2.fill(new RoundRectangle2D.Float(0, 0, getWidth(), getHeight(), 10, 10)); + g2.dispose(); + } + }; + + JLabel label = new JLabel("Skip injection"); + label.setFont(FONT_BODY); + label.setForeground(TEXT_SECONDARY); + label.setHorizontalAlignment(SwingConstants.CENTER); + btn.add(label, BorderLayout.CENTER); + btn.setAlignmentX(Component.LEFT_ALIGNMENT); + return btn; + } + + private static Icon createImageIcon(String resourcePath) { + return new Icon() { + private BufferedImage masked; + + { + try { + BufferedImage raw = ImageIO.read(DebuggerPicker.class.getResourceAsStream(resourcePath)); + masked = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); + Graphics2D mg = masked.createGraphics(); + mg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + mg.setColor(Color.WHITE); + mg.fill(new RoundRectangle2D.Float(0, 0, 32, 32, 8, 8)); + mg.setComposite(AlphaComposite.SrcIn); + mg.drawImage(raw, 0, 0, null); + mg.dispose(); + } catch (Exception e) { + masked = null; + } + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + if (masked != null) { + g.drawImage(masked, x, y, null); + } + } + + @Override + public int getIconWidth() { + return 32; + } + + @Override + public int getIconHeight() { + return 32; + } + }; + } + + public DebuggerSelection getSelection() { + setVisible(true); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return selectedDebugger; + } +} diff --git a/src/main/java/dev/xirreal/GfxDebuggers.java b/src/main/java/dev/xirreal/GfxDebuggers.java index 38ac73b..ffa915d 100644 --- a/src/main/java/dev/xirreal/GfxDebuggers.java +++ b/src/main/java/dev/xirreal/GfxDebuggers.java @@ -1,21 +1,22 @@ package dev.xirreal; +import static dev.xirreal.DebuggerPicker.DebuggerSelection; import static dev.xirreal.PlatformUtils.*; +import java.awt.*; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; +import java.lang.management.ManagementFactory; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import javax.swing.*; +import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; +import org.lwjgl.util.tinyfd.TinyFileDialogs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,79 +40,95 @@ public void onPreLaunch() { return; } - String renderdocMarker = System.getenv(RENDERDOC_MARKER_ENV); - if ("1".equals(renderdocMarker)) { - LOGGER.info("Game was re-launched with RenderDoc LD_PRELOAD marker. Checking for library..."); - if (IS_LINUX && isLibraryLoaded("librenderdoc")) { - LOGGER.info("Renderdoc injection successful."); - } else if (IS_LINUX) { - LOGGER.warn("Renderdoc marker found but library not loaded. Injection failed."); + if (System.getenv(RENDERDOC_MARKER_ENV) != null) { + LOGGER.info("Process relaunched with Renderdoc marker. Checking if library is loaded..."); + if (isLibraryLoaded("librenderdoc")) { + LOGGER.info("Renderdoc library is loaded. Continuing with normal launch."); + } else { + LOGGER.error("Renderdoc marker environment variable is set but library is not loaded. Something went wrong with the injection."); + throw new IllegalStateException("Renderdoc injection failed"); } return; + } else if (System.getenv(NSIGHT_MARKER_ENV) != null) { + LOGGER.info("Process relaunched with NSight Graphics marker. Continuing with normal launch."); + return; } - String nsightMarker = System.getenv(NSIGHT_MARKER_ENV); - if ("1".equals(nsightMarker)) { - LOGGER.info("Game was launched via ngfx. NSight Graphics injection is active."); - return; + ProcessHandle processHandle = ProcessHandle.current(); + String javaExecutable = processHandle.info().command().orElse("java"); + List jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments(); + + List fullArgs = new ArrayList<>(); + fullArgs.addAll(jvmArgs); + if (System.getProperty("java.library.path") != null) { + fullArgs.add("-Djava.library.path=" + System.getProperty("java.library.path")); } + fullArgs.add("-cp"); + fullArgs.add(System.getProperty("java.class.path")); + + // Bypass launcher shims and launch fabric directly as god intended + fullArgs.add("net.fabricmc.loader.impl.launch.knot.KnotClient"); + + String[] args = FabricLoader.getInstance().getLaunchArguments(false); + fullArgs.addAll(Arrays.asList(args)); + + DebuggerSelection choice = DebuggerSelection.NONE; - int option = -1; String optionString = System.getProperty("debugger"); if (optionString != null) { if (optionString.equalsIgnoreCase("renderdoc")) { - option = JOptionPane.CANCEL_OPTION; + choice = DebuggerSelection.RENDERDOC; } else if (optionString.equalsIgnoreCase("nsight-gpu")) { - option = JOptionPane.YES_OPTION; + choice = DebuggerSelection.GPU_TRACE; } else if (optionString.equalsIgnoreCase("nsight-frame")) { - option = JOptionPane.NO_OPTION; + choice = DebuggerSelection.FRAME_DEBUGGER; } } - if (option == -1) { - System.setProperty("java.awt.headless", "false"); + if (choice == DebuggerSelection.NONE) { + String originalHeadless = System.getProperty("java.awt.headless"); + String originalAA = System.getProperty("awt.useSystemAAFontSettings"); + String originalAAText = System.getProperty("swing.aatext"); try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (ReflectiveOperationException | UnsupportedLookAndFeelException ignored) {} - - String[] options = { "NSight GPU Trace", "NSight Frame Profiler", "Renderdoc" }; - - JFrame frame = new JFrame("Choose a debugger to be loaded"); - frame.setUndecorated(true); - frame.setVisible(true); - frame.setLocationRelativeTo(null); - frame.requestFocus(); - - option = JOptionPane.showOptionDialog( - frame, - "Closing the dialog will skip injection.\n\nNSight or Renderdoc must be installed for this to work properly.", - "Shader debugging", - JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.QUESTION_MESSAGE, - null, - options, - null - ); - - frame.dispose(); - System.setProperty("java.awt.headless", "true"); - } - - if (option == JOptionPane.CLOSED_OPTION) { - LOGGER.info("Modal closed, skipping injection..."); - return; + System.setProperty("java.awt.headless", "false"); + System.setProperty("awt.useSystemAAFontSettings", "on"); + System.setProperty("swing.aatext", "true"); + + DebuggerPicker picker = new DebuggerPicker(); + choice = picker.getSelection(); + } catch (Exception e) { + LOGGER.error("Could not open Swing window. Falling back to command line selection.", e); + } finally { + if (originalHeadless != null) { + System.setProperty("java.awt.headless", originalHeadless); + } else { + System.clearProperty("java.awt.headless"); + } + if (originalAA != null) { + System.setProperty("awt.useSystemAAFontSettings", originalAA); + } else { + System.clearProperty("awt.useSystemAAFontSettings"); + } + if (originalAAText != null) { + System.setProperty("swing.aatext", originalAAText); + } else { + System.clearProperty("swing.aatext"); + } + } } - if (option == JOptionPane.CANCEL_OPTION) { - injectRenderdoc(); + if (choice == DebuggerSelection.NONE) { + LOGGER.warn("Injection skipped! No debugger will be injected and the game will launch normally."); return; + } else if (choice == DebuggerSelection.RENDERDOC) { + launchRenderdoc(javaExecutable, fullArgs); + } else { + launchViaNgfx(javaExecutable, fullArgs, choice); } - - launchViaNgfx(option); } - private void injectRenderdoc() { + private void launchRenderdoc(String javaExecutable, List args) { LOGGER.info("Injecting Renderdoc..."); try { if (IS_LINUX) { @@ -119,7 +136,7 @@ private void injectRenderdoc() { if (renderdocPath == null) { LOGGER.error("Renderdoc library not found. Checked standard system paths."); LOGGER.error("Set -Drenderdoc.path= or RENDERDOC_PATH env var to your RenderDoc install directory or librenderdoc.so path."); - return; + throw new IllegalStateException("Renderdoc library not found"); } LOGGER.info("Found Renderdoc at: {}", renderdocPath); @@ -128,32 +145,30 @@ private void injectRenderdoc() { return; } - Map env = new HashMap<>(); - env.put(RENDERDOC_MARKER_ENV, "1"); - - if (reExecWithPreload(renderdocPath, env)) { - LOGGER.error("Re-exec with LD_PRELOAD failed. RenderDoc cannot be injected."); - LOGGER.error("Try launching the game manually with: LD_PRELOAD={} ", renderdocPath); + if (!relaunchWithExtraLD_PRELOAD(javaExecutable, args, renderdocPath, RENDERDOC_MARKER_ENV)) { + LOGGER.error("Re-exec with LD_PRELOAD failed."); + LOGGER.error("Try launching the game manually with this environment variable set: LD_PRELOAD={}", renderdocPath); + throw new IllegalStateException("Failed to relaunch with Renderdoc"); } } else { String renderdocDll = RenderdocLocator.findRenderdocDll(); if (renderdocDll == null) { LOGGER.error("Renderdoc installation not found in common paths."); LOGGER.error("Set -Drenderdoc.path= or RENDERDOC_PATH env var to your RenderDoc install directory."); - return; + throw new IllegalStateException("Renderdoc DLL not found"); } - LOGGER.info("Found Renderdoc installation at: {}", renderdocDll); + LOGGER.info("Found Renderdoc shared library at: {}", renderdocDll); System.load(renderdocDll); LOGGER.info("Renderdoc loaded successfully."); } - } catch (UnsatisfiedLinkError e) { - LOGGER.error("Failed to load Renderdoc: ", e); + } catch (Exception e) { + LOGGER.error("Failed to launch with Renderdoc: ", e); + throw new IllegalStateException("Failed to launch with Renderdoc", e); } } - private void launchViaNgfx(int option) { - String activity = (option == JOptionPane.YES_OPTION) ? "GPU Trace Profiler" : "Frame Debugger"; - LOGGER.info("Launching game via ngfx CLI for {}...", activity); + private void launchViaNgfx(String exe, List args, DebuggerSelection activity) { + LOGGER.info("Launching game via ngfx CLI for {}...", activity.name()); Path ngfx = NgfxLocator.findNgfxExecutable(); if (ngfx == null) { @@ -163,60 +178,38 @@ private void launchViaNgfx(int option) { } else { LOGGER.error("Expected in: Program Files/NVIDIA Corporation/Nsight Graphics */host/windows-desktop-nomad-x64/ngfx.exe"); } - return; + throw new IllegalStateException("ngfx executable not found"); } - LOGGER.info("Found ngfx at: {}", ngfx); - String exe; - try { - exe = getCurrentExe(); - } catch (IOException e) { - LOGGER.error("Failed to read current process exe: ", e); - return; - } + LOGGER.info("Found ngfx at: {}", ngfx); String workDir = System.getProperty("user.dir"); - String platform = IS_LINUX ? "Linux (x86_64)" : "Windows"; List cmd = new ArrayList<>(); cmd.add(ngfx.toAbsolutePath().toString()); - cmd.add("--activity=" + activity); - cmd.add("--platform=" + platform); + cmd.add("--activity=" + (activity == DebuggerSelection.GPU_TRACE ? "GPU Trace Profiler" : "Frame Debugger")); cmd.add("--exe=" + exe); + Path argFile = null; try { - List rawArgs = readCurrentArgsList(); - if (!rawArgs.isEmpty()) { - List javaArgs = filterJavaArgs(rawArgs); - Path argFile = writeArgFile(javaArgs); - LOGGER.info("Wrote {} args to argfile: {}", javaArgs.size(), argFile); - cmd.add("--args=@" + argFile.toAbsolutePath()); - } - } catch (IOException e) { - LOGGER.error("Failed to create argfile, falling back to inline args", e); - try { - String argsString = getCurrentArgsString(); - argsString = stripTheseusFromArgsString(argsString); - if (!argsString.isEmpty()) { - if (IS_WINDOWS) { - argsString = argsString.replace("\"", "\\\""); - } - cmd.add("--args=" + argsString); - } - } catch (IOException e2) { - LOGGER.error("Failed to read current args: ", e2); - return; - } + argFile = writeArgFile(args); + cmd.add("--args=@" + argFile.toAbsolutePath()); + } catch (Exception e) { + LOGGER.error("Failed to create argfile for ngfx: ", e); + throw new IllegalStateException("Failed to create argfile for ngfx", e); } cmd.add("--dir=" + workDir); - cmd.add("--env=" + NSIGHT_MARKER_ENV + "=1;"); + cmd.add("--env=" + NSIGHT_MARKER_ENV + "=1"); cmd.add("--launch-detached"); - if (activity.equals("GPU Trace Profiler")) { + if (activity == DebuggerSelection.GPU_TRACE) { + cmd.add("--limit-to-frames"); + cmd.add("5"); + cmd.add("--multi-pass-metrics"); cmd.add("--start-after-hotkey"); } - LOGGER.info("Running ngfx: {}", cmd); + LOGGER.info("Running ngfx with command: {}", String.join(" ", cmd)); try { ProcessBuilder pb = new ProcessBuilder(cmd); @@ -236,104 +229,29 @@ private void launchViaNgfx(int option) { if (exitCode != 0) { LOGGER.error("ngfx exited with code {}. Check that NSight Graphics is installed correctly.", exitCode); - return; + throw new IllegalStateException("ngfx exited with code " + exitCode); } LOGGER.info("Game re-launched via ngfx (exit code 0). Terminating current process."); System.exit(0); } catch (IOException e) { LOGGER.error("Failed to run ngfx: ", e); + throw new IllegalStateException("Failed to run ngfx", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOGGER.error("Interrupted while waiting for ngfx: ", e); - } - } - - private static List filterJavaArgs(List args) { - List filtered = new ArrayList<>(); - for (int i = 0; i < args.size(); i++) { - String arg = args.get(i); - - if (arg.matches("-javaagent:.*theseus\\.jar.*")) { - LOGGER.info("Stripping theseus javaagent: {}", arg); - continue; - } - if (arg.equals("com.modrinth.theseus.MinecraftLaunch")) { - LOGGER.info("Stripping theseus main class"); - continue; - } - if (arg.startsWith("-Dmodrinth.internal.")) { - LOGGER.info("Stripping Modrinth property: {}", arg); - continue; - } - - if ((arg.equals("-cp") || arg.equals("-classpath")) && i + 1 < args.size()) { - filtered.add(arg); - i++; - String cp = args.get(i); - String separator = IS_WINDOWS ? ";" : ":"; - String cleaned = Arrays.stream(cp.split(IS_WINDOWS ? ";" : ":")) - .filter(entry -> !entry.contains("theseus.jar")) - .collect(Collectors.joining(separator)); - if (!cleaned.equals(cp)) { - LOGGER.info("Stripped theseus.jar from classpath"); + throw new IllegalStateException("Interrupted while waiting for ngfx", e); + } catch (Exception e) { + LOGGER.error("Failed to launch with NSight Graphics: ", e); + throw new IllegalStateException("Failed to launch with NSight Graphics", e); + } finally { + if (argFile != null) { + try { + Files.deleteIfExists(argFile); + } catch (IOException e) { + LOGGER.error("Failed to delete temporary arg file: {}", argFile, e); } - filtered.add(cleaned); - continue; } - - filtered.add(arg); } - return filtered; - } - - private static Path writeArgFile(List args) throws IOException { - Path argFile = Files.createTempFile("gfx-debuggers-", ".args"); - try (BufferedWriter writer = Files.newBufferedWriter(argFile)) { - for (String arg : args) { - writer.write(quoteForArgFile(arg)); - writer.newLine(); - } - } - return argFile; - } - - private static String quoteForArgFile(String arg) { - if (arg.isEmpty()) { - return "\"\""; - } - boolean needsQuoting = false; - for (int i = 0; i < arg.length(); i++) { - char c = arg.charAt(i); - if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '"' || c == '\'' || c == '\\' || c == '#') { - needsQuoting = true; - break; - } - } - if (!needsQuoting) { - return arg; - } - StringBuilder sb = new StringBuilder(arg.length() + 8); - sb.append('"'); - for (int i = 0; i < arg.length(); i++) { - char c = arg.charAt(i); - if (c == '\\' || c == '"') { - sb.append('\\'); - } - sb.append(c); - } - sb.append('"'); - return sb.toString(); - } - - private static String stripTheseusFromArgsString(String argsString) { - argsString = argsString.replaceAll("-javaagent:[^\\s]+theseus\\.jar", ""); - argsString = argsString.replaceAll("com\\.modrinth\\.theseus\\.MinecraftLaunch", ""); - argsString = argsString.replaceAll("-Dmodrinth\\.internal\\.[^\\s]+", ""); - argsString = argsString.replaceAll("[^\\s:;]*theseus\\.jar[;:]?", ""); - argsString = argsString.replaceAll("::+", ":").replaceAll(";;+", ";"); - argsString = argsString.replaceAll("(-cp\\s+|--classpath\\s+)[;:]", "$1"); - argsString = argsString.replaceAll(" +", " ").trim(); - return argsString; } } diff --git a/src/main/java/dev/xirreal/PlatformUtils.java b/src/main/java/dev/xirreal/PlatformUtils.java index b3817b4..219f861 100644 --- a/src/main/java/dev/xirreal/PlatformUtils.java +++ b/src/main/java/dev/xirreal/PlatformUtils.java @@ -4,11 +4,15 @@ import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.StringArray; +import java.io.*; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.List; import java.util.Map; public class PlatformUtils { @@ -22,12 +26,6 @@ public interface LibC extends Library { int execv(String pathname, StringArray argv); } - public interface Kernel32 extends Library { - Pointer GetCommandLineW(); - } - - public static final Kernel32 KERNEL32 = IS_WINDOWS ? Native.load("kernel32", Kernel32.class) : null; - public static boolean isLibraryLoaded(String needle) { try { String maps = Files.readString(Paths.get("/proc/self/maps")); @@ -37,169 +35,71 @@ public static boolean isLibraryLoaded(String needle) { } } - public static List readCurrentCmdline() throws IOException { - byte[] cmdlineBytes = Files.readAllBytes(Paths.get("/proc/self/cmdline")); - List args = new ArrayList<>(); - int start = 0; - for (int i = 0; i < cmdlineBytes.length; i++) { - if (cmdlineBytes[i] == 0) { - if (i > start) { - args.add(new String(cmdlineBytes, start, i - start)); - } - start = i + 1; - } - } - if (start < cmdlineBytes.length) { - args.add(new String(cmdlineBytes, start, cmdlineBytes.length - start)); + public static boolean relaunchWithExtraLD_PRELOAD(String exe, List args, String preloadLib, String markerEnvVar) { + String currentPreload = System.getenv("LD_PRELOAD"); + String newPreload; + if (currentPreload != null && !currentPreload.isEmpty()) { + newPreload = preloadLib + ":" + currentPreload; + } else { + newPreload = preloadLib; } - return args; - } - public static boolean reExecWithPreload(String preloadLib, Map extraEnv) { - try { - List args = readCurrentCmdline(); - if (args.isEmpty()) { - GfxDebuggers.LOGGER.error("Failed to read /proc/self/cmdline: no arguments found"); - return true; - } + LibC.INSTANCE.setenv("LD_PRELOAD", newPreload, 1); + LibC.INSTANCE.setenv(markerEnvVar, "1", 1); - String exe = Paths.get("/proc/self/exe").toRealPath().toString(); - - String currentPreload = System.getenv("LD_PRELOAD"); - String newPreload; - if (currentPreload != null && !currentPreload.isEmpty()) { - newPreload = preloadLib + ":" + currentPreload; - } else { - newPreload = preloadLib; - } - - LibC.INSTANCE.setenv("LD_PRELOAD", newPreload, 1); - - if (extraEnv != null) { - for (Map.Entry entry : extraEnv.entrySet()) { - LibC.INSTANCE.setenv(entry.getKey(), entry.getValue(), 1); - } - } + GfxDebuggers.LOGGER.info("Replacing process..."); - GfxDebuggers.LOGGER.info("Re-executing process with LD_PRELOAD={}", newPreload); - GfxDebuggers.LOGGER.info("Executable: {}", exe); - GfxDebuggers.LOGGER.info("Arguments: {}", args); + StringArray argv = new StringArray(args.toArray(new String[0])); + LibC.INSTANCE.execv(exe, argv); - StringArray argv = new StringArray(args.toArray(new String[0])); - LibC.INSTANCE.execv(exe, argv); - - int errno = Native.getLastError(); - GfxDebuggers.LOGGER.error("execv failed with errno {}", errno); - return true; - } catch (IOException e) { - GfxDebuggers.LOGGER.error("Failed to re-exec with LD_PRELOAD: ", e); - return true; - } + int errno = Native.getLastError(); + GfxDebuggers.LOGGER.error("execv failed with errno {}", errno); + return false; } - public static List readCurrentArgsList() throws IOException { - if (IS_LINUX) { - List cmdline = readCurrentCmdline(); - if (cmdline.size() <= 1) return List.of(); - return new ArrayList<>(cmdline.subList(1, cmdline.size())); - } else { - String fullCmd = KERNEL32.GetCommandLineW().getWideString(0); - List all = parseWindowsCommandLine(fullCmd); - if (all.size() <= 1) return List.of(); - return new ArrayList<>(all.subList(1, all.size())); - } - } + static Path writeArgFile(List args) throws IOException { + Path argFile = Files.createTempFile("gfx-debuggers-", ".args"); + + argFile.toFile().deleteOnExit(); - public static List parseWindowsCommandLine(String commandLine) { - List args = new ArrayList<>(); - if (commandLine == null || commandLine.isEmpty()) return args; - - StringBuilder current = new StringBuilder(); - boolean inQuotes = false; - int i = 0; - - while (i < commandLine.length()) { - char c = commandLine.charAt(i); - - if (c == '\\') { - int numBackslashes = 0; - while (i < commandLine.length() && commandLine.charAt(i) == '\\') { - numBackslashes++; - i++; - } - if (i < commandLine.length() && commandLine.charAt(i) == '"') { - for (int j = 0; j < numBackslashes / 2; j++) { - current.append('\\'); - } - if (numBackslashes % 2 == 1) { - current.append('"'); - i++; - } - } else { - for (int j = 0; j < numBackslashes; j++) { - current.append('\\'); - } - } - } else if (c == '"') { - inQuotes = !inQuotes; - i++; - } else if ((c == ' ' || c == '\t') && !inQuotes) { - if (current.length() > 0) { - args.add(current.toString()); - current.setLength(0); - } - i++; - } else { - current.append(c); - i++; + try (BufferedWriter writer = Files.newBufferedWriter(argFile, StandardCharsets.UTF_8)) { + for (String arg : args) { + writer.write(quoteForArgFile(arg)); + writer.newLine(); } } - if (current.length() > 0) { - args.add(current.toString()); - } - return args; + return argFile; } - public static String getCurrentExe() throws IOException { - if (IS_LINUX) { - return Paths.get("/proc/self/exe").toRealPath().toString(); - } else { - return ProcessHandle.current().info().command().orElse(Paths.get(System.getProperty("java.home"), "bin", "java.exe").toString()); + // https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html#java-command-line-argument-files + static String quoteForArgFile(String arg) { + if (arg.isEmpty()) { + return "\"\""; } - } - public static String getCurrentArgsString() throws IOException { - if (IS_LINUX) { - List cmdline = readCurrentCmdline(); - if (cmdline.size() <= 1) return ""; - StringBuilder sb = new StringBuilder(); - for (int i = 1; i < cmdline.size(); i++) { - if (i > 1) sb.append(' '); - sb.append(shellQuote(cmdline.get(i))); + boolean needsQuoting = false; + for (int i = 0; i < arg.length(); i++) { + char c = arg.charAt(i); + if (Character.isWhitespace(c) || c == '"' || c == '\'' || c == '\\' || c == '#') { + needsQuoting = true; + break; } - return sb.toString(); - } else { - String fullCmd = KERNEL32.GetCommandLineW().getWideString(0); - return stripExeFromCommandLine(fullCmd); } - } - public static String shellQuote(String arg) { - if (arg.isEmpty()) return "''"; - if (arg.matches("[a-zA-Z0-9._/=:@%,+-]+")) return arg; - return "'" + arg.replace("'", "'\\''") + "'"; - } + if (!needsQuoting) { + return arg; + } - public static String stripExeFromCommandLine(String cmdLine) { - if (cmdLine == null || cmdLine.isEmpty()) return ""; - String trimmed = cmdLine.trim(); - if (trimmed.startsWith("\"")) { - int endQuote = trimmed.indexOf('"', 1); - if (endQuote >= 0) { - return trimmed.substring(endQuote + 1).trim(); + StringBuilder sb = new StringBuilder(arg.length() + 8); + sb.append('"'); + for (int i = 0; i < arg.length(); i++) { + char c = arg.charAt(i); + if (c == '\\' || c == '"') { + sb.append('\\'); } + sb.append(c); } - int space = trimmed.indexOf(' '); - return space >= 0 ? trimmed.substring(space + 1).trim() : ""; + sb.append('"'); + return sb.toString(); } } diff --git a/src/main/resources/assets/gfx-debuggers/frame-debugger.png b/src/main/resources/assets/gfx-debuggers/frame-debugger.png new file mode 100644 index 0000000000000000000000000000000000000000..22829640dc7c482489f0b159aaeab41c4832c813 GIT binary patch literal 2303 zcmZ9Nc{J1w7sr3iSSAk|m7yBTSTbdqLB<#~MkB^f$`X|@8e?P`*+!OlY$L^xhf&7b zLg+E}HIq(c*T_>O(%6ZTdZI_?eb0N&^PYR|z2|=K`J8*t{pY6G+nP&=9To!sK!RjJ zbllB~zZ@d68$%)^KmRJcEF5hBAo3^x#3lg1_gyJ=4gf;Y0I+Zg0C2egAQQ-LaxmI$ zh+MWZCjvizSz&8=)~+H-C)t>ZE`TIpAT29;VDi-Z=#H*N2RKJl z7|9MnN+>?NW+6BR6Brdv86kY6LiGYC_Ndv>ZVAY~;i|oRSdLgxCCS8kI2`$jpZe@j zl6E{7)Kg@dre(m=>h~BAULVbz6@f|zzfzU;5(~qvM$hhzQ`I-{`V#e8`@1yz&n5h( za=%s!vGE~9xru)f42M3Qt&Ts3P>#-*ogXLIo#?P;FduxBI{ZSV`-6)YX@vb(k9J+< zH#mTji_S@G6qh@Vl)ypl$3!lkHh~`ZEXb&g&b2vp&rGWN)&M$_cDbZ3yLi<&1M4zT zUl3WFb8;KuRtbCMhw9?mu-WpZ#`tsyt0zx!6j>S+zGkFvhD@sz`mzo4M7rqd64>6RT)!v=ppBw6rH zm1bZ!dZ37PO8}z^%R$_JvrMT{A~wlGYnc420zSvsooN?5y}jM?220c}P~mRm8a?-s zrt=53){LEsj`xoTNG1_@d7XU~hZro{;^tbCbNj&j_?)VyGL&Nf+xT6y^?>F!YBJbn_!SPyu`=U0&N5P zgIe`mns0ahl@2f~o6_z4!*2;D8O&;p`z8=E?+Fr~H)O3wQOP>-MnHNAcbz2He)l}t zzMQm6H$7PBtwCy+R#}Nmwb9;rhtdow8ZO{z29&M_D}rZ*BvDGTRK9__%ZL@;W8JI6 z7d^rrI1=qGIreN?TereKe9!FzF-mnU_T;@dC~CLEEZ_OAcRhOQ&C7b<$B=-6_h0J22G4D+wlb}r z%MKQt2E}KyH`0s6&jyq}4j^}RXz?ga?GF)?+zUKJfBT&%rKfk9&XeDSV=z=)9&Ltvskh6t|7t1uFRuY`Dde52q5NE}UFUnAltuRB{d=76vg%ZQdoA+> zPWcjhF@UPcH(=IaeUD{Jr!;qbe*4KsKG{-w;T3@;BE5BY7y;Fij7z?1GC3cb zN&8$0l3dq#{sCscQ%o4ruB!E(KjZBHNw8WDLF994&^Fu?PT~p=^S71axs#Csqw#*3 z0<7Nq<0itlNQ{O;oOE0 zR()pJw{88B5f`^0>!!{+H&Ic~-=`cwesKRtL)6SE2BTNLExzp!t3b8x|fw874P~D17wZmZ%+dp*u6}Ch?WsA5X!qad*E;uWz^bmMRbLs86 z+37Ln1?N;0W?+5#5x&!9kz1cjIhABqT+^*qJZvi4`Cx0nsqE_I5{4+w-a}$8T+zny zDOy8hbg@b%!{}MFZq-op@M7!&i!-(-ohw<&DBf$eC!+D$5PzgL@0j{$&Fea_PMrB& zfeOjAl^=GtBDeJ5<)L1XQ~j+Ww=ZI*cw&;;rC7%j*q!jxwtAE7ZH_n_pp$IZ^QC_dzTri zdzj$bS!}0N$PZ*j6?_Xk~h@f6Z{MmPwA}R4K&oH=Ao&LvlwWQAAsW?Vy{op z#@tZ(T?N<2$!hO9+{K8zX(?^l3e7QYsGbw+2o40kw6=!&O7x_3;;NP>9=T|D^%kL$ z!M+H)&XQE4Pd#NUP)Svk z6$Af)4?`;iW{x=7@ge-I9-GPGAMl0 PueC^Kw#3J#m*W2cG07V( literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/gfx-debuggers/gpu-trace.png b/src/main/resources/assets/gfx-debuggers/gpu-trace.png new file mode 100644 index 0000000000000000000000000000000000000000..f479904969e33e93f8e179ff986a6ef1bdc3edf1 GIT binary patch literal 2285 zcmZvdc`($E8^=HHb6Gd#zU5k*^MAv2NCp+t*L?o%zjtXMXd{^StMIKl7UBpJ(QIWM^XrVn4$U0077wiEubo$v?@; zd|JaXnC(A}FVevp05I|Z5cd!O4o>s<~9A^7ui8Xv7BhKT)W>SV*N=RNKD_i!Oz+xl=D(uF%G zqrX1<9i+ZLrmQSZe~Vt4)SQfHmX2J92!fgH5%lNSd4Z?A-{z#=C2n6(tm>r)t6&W? z!9qtntb1otOGt989noMwRo&!%D)#syrJT{mou-xYvy2%M(P3g2kAIAF7O?}Z%*QSJ zV<&jEyT~Ok5HaIr*rZGf0}|PmW!KQxUgn6i9ZE+mHndxEeTNw^T!--6D0lru%#l5L z#FOb{f&7vqso}$*1r=uS;pYMGqiF}sijnKClTB$|Nh5W4*Coi>k!Mw(h|PVGzopb= zbU$}YL{_&mt#Ws7Vb3kudTp*zTw%z&2c_D5CG~)U6l36L^$ZAdx9}QeZW1c?_WnB8 z_y#>#_j_SO!*ilFv$mFt#b_WIu9s(bU1^QLuSLF={;6{c>0y7f9n z1+7`@-MJLB7`QCO2ZOp5piUOFc0@609St8o>R77z?b5a)uhoe_%IR%Pj;jLQDU>~~ zZtkbk?wss~#w~5&uIRy~$${-AFDpA+yQf|C_?qlskBuP0#9vQ)SdZ~A?yT(SEj3(?Deb65*&tEE0lkd@{W@*SU7MY$d(IM#!R3KyrR zh1RYOF})3)8`pJh3ITCYcQ>bejc*7y=XxZHSGZ_D9>AiI!%iKJ?R@G_=<@BajH}d$ zWySa--YZK_W$NECZEW)I(InUV98e;uZw~jokHn~NRix`(nX~WZ*1S3@I4mYS8gUnT zR~V#3!rl>3tHLS;=r|fx&!qLpjL3}KkHAIXeha@8eu*>RTzKF++B#V8J5}6#yOZ1H z$5=6<3nAHO#uqK38=CsJ$?}EY`-Ai{CvCcQMJP~oA0?{Esnn6`PEPHcPmwU7K6F7bH$S^1nO_7=D|6VZoLzuO1Qzs;^GnfqB?cdd7LataY*2 zJP#S$8Fs1OY<8UaAZt{n+z^t8K-VGxmA&-%Wvd#nSyuhd_-v(WLVv%Oxy{hA-el}#E>Fs7&cMixiToMPQ1H!XdG z!EfTpFw~8?w>O4+Du>MWel@i!g``tO$%>UHypehIlB|=52+s%Ra+g>=l}HJt9`P-5 zN&Sc+{dzclpjxM&b*)PtIg7YMkW5)Y%_Mqc#o_;l6s&UC@4l ziF@M`T(I%?>-~?q)QmhBPq&Lz9+@m4d;t>|yAqXgdFMiZviNf)?y>aUVQhtyBx3~* z3D;CKZHR$xs7bUO<3H~8?t*mbxn&*lu!FR=@u13aN25S$E@ir*gAS&) z6U%X?b;;I=dSsos*<9fi?mrMd& zFuzj)pb)5*Dg>&kao!QC0f)ihFl}WB1P+0q^6iq1zd~zKLGR}poA}H^%PM2 p-v)HRO}}vT)te#zVxVvc6n89|9x#|-`@TD z`^TM6&+mPH@$JvgBQLLc?rC0dXW#j^_wr87@!Heuy|<n;jFTTIO{^5z|o~Eo5Gr#}&x%A$_kH5afADLvit^D`jKNGHPo_l*w z#)%ne$EUA-c-(eJ)vjk3-v9g(b9ln7kI!Cx|Co7VM$zecAAWvy-Q6(p`j$Wc{&rkm z7k#)t_V9$htDByEd)IPlwaxabq@$DXeSZ1(-@kwV|6O_iDC$t3!_J!gQ**aHIs5qQ zn~jf7=A4{mzoTZy)AK+6{CfBE^OLV{ZhUlOEpv|U=0eR5X7 z{&x4>jmz#GI`{VOhhJYS&n=ESGSPWgz16k~=UsLE*Ea2VdM^Le9NQh$Zo3=pc2uh~ zUC;+c17ni6yNkfoO}sil4tt5GuPgg2CSEZn^%^t5OF(HAPZ!4!i_=dhRhv6GiX44D zuWiq|GD$BF*Gb`%whL)Z|Fp+tiNHzY_wV!9W-P4!&GL4C+O?l&<9^RE?zuW&C5eq$ zd1csKa}%lAv!bJJSM=_>lgGrcd8zYsfw{2_SFh!=S5>vjU1YcTHCOLDgTabqMH}Z2 ze7{>h^!6m>#AG`P*ZI`hnu~Wyoy*XO9Dz;=xP85~r zzw}BEg>k?vuB7|Ne9M{j|%|Zafa|?C9QP zAz071f6uAvZrIGp!Q02IYx+PXaY) pz-=hW%uOvWNz5%k(PLy~U}R+swM0hWeiu*=gQu&X%Q~loCICLs(!2lw literal 0 HcmV?d00001 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index a5e2c17..0cdbce5 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -15,8 +15,7 @@ "preLaunch": ["dev.xirreal.GfxDebuggers"] }, "depends": { - "fabricloader": ">=0.15.0", - "minecraft": "^1.18.2", + "fabricloader": ">=0.14.0", "java": ">=17" } } From e9516429ca5fb29c6ff9f0b7be7d165985913370 Mon Sep 17 00:00:00 2001 From: xirreal Date: Sat, 14 Feb 2026 01:03:59 +0100 Subject: [PATCH 2/2] CI still needs java 21 --- .github/workflows/build.yml | 52 ++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ae9cc6..5190842 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,30 +7,28 @@ name: build on: [pull_request, push] jobs: - build: - strategy: - matrix: - java: [ - 17, - ] - runs-on: ubuntu-22.04 - steps: - - name: checkout repository - uses: actions/checkout@v4 - - name: validate gradle wrapper - uses: gradle/wrapper-validation-action@v2 - - name: setup jdk ${{ matrix.java }} - uses: actions/setup-java@v4 - with: - java-version: ${{ matrix.java }} - distribution: 'microsoft' - - name: make gradle wrapper executable - run: chmod +x ./gradlew - - name: build - run: ./gradlew build - - name: capture build artifacts - if: ${{ matrix.java == '17' }} - uses: actions/upload-artifact@v4 - with: - name: Artifacts - path: build/libs/ \ No newline at end of file + build: + strategy: + matrix: + java: [21] + runs-on: ubuntu-22.04 + steps: + - name: checkout repository + uses: actions/checkout@v4 + - name: validate gradle wrapper + uses: gradle/wrapper-validation-action@v2 + - name: setup jdk ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java }} + distribution: "microsoft" + - name: make gradle wrapper executable + run: chmod +x ./gradlew + - name: build + run: ./gradlew build + - name: capture build artifacts + if: ${{ matrix.java == '21' }} + uses: actions/upload-artifact@v4 + with: + name: Artifacts + path: build/libs/