diff --git a/patches/net/minecraft/client/multiplayer/ClientDebugSubscriber.java.patch b/patches/net/minecraft/client/multiplayer/ClientDebugSubscriber.java.patch
new file mode 100644
index 0000000000..62e2657c96
--- /dev/null
+++ b/patches/net/minecraft/client/multiplayer/ClientDebugSubscriber.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/client/multiplayer/ClientDebugSubscriber.java
++++ b/net/minecraft/client/multiplayer/ClientDebugSubscriber.java
+@@ -64,6 +_,8 @@
+ addFlag(subscriptions, DebugSubscriptions.VILLAGE_SECTIONS, SharedConstants.DEBUG_VILLAGE_SECTIONS);
+ }
+
++ // Neo: Allow requesting custom subscriptions
++ net.neoforged.neoforge.common.NeoForge.EVENT_BUS.post(new net.neoforged.neoforge.client.event.AddDebugSubscriptionFlagsEvent((subscription, flag) -> addFlag(subscriptions, subscription, flag)));
+ return subscriptions;
+ }
+
diff --git a/patches/net/minecraft/client/renderer/debug/DebugRenderer.java.patch b/patches/net/minecraft/client/renderer/debug/DebugRenderer.java.patch
new file mode 100644
index 0000000000..580ea21a51
--- /dev/null
+++ b/patches/net/minecraft/client/renderer/debug/DebugRenderer.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/client/renderer/debug/DebugRenderer.java
++++ b/net/minecraft/client/renderer/debug/DebugRenderer.java
+@@ -137,6 +_,9 @@
+ }
+
+ this.renderers.add(new ChunkCullingDebugRenderer(minecraft));
++
++ // Neo: Allow registering custom debug renderers
++ net.neoforged.fml.ModLoader.postEvent(new net.neoforged.neoforge.client.event.RegisterDebugRenderersEvent(factory -> renderers.add(factory.apply(minecraft))));
+ }
+
+ public void emitGizmos(Frustum frustum, double camX, double camY, double camZ, float partialTicks) {
diff --git a/src/client/java/net/neoforged/neoforge/client/event/AddDebugSubscriptionFlagsEvent.java b/src/client/java/net/neoforged/neoforge/client/event/AddDebugSubscriptionFlagsEvent.java
new file mode 100644
index 0000000000..4a138f1f75
--- /dev/null
+++ b/src/client/java/net/neoforged/neoforge/client/event/AddDebugSubscriptionFlagsEvent.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.neoforged.neoforge.client.event;
+
+import it.unimi.dsi.fastutil.objects.ObjectBooleanBiConsumer;
+import java.util.function.Supplier;
+import net.minecraft.client.multiplayer.ClientDebugSubscriber;
+import net.minecraft.util.debug.DebugSubscription;
+import net.neoforged.bus.api.Event;
+import net.neoforged.fml.LogicalSide;
+import net.neoforged.fml.loading.FMLEnvironment;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * This event allows mods to register client-side {@linkplain DebugSubscription debug subscription} flags.
+ *
+ * These flags are used to determine when subscriptions are allowed to register and store debug renderer values.
+ * If no flag is registered for a given debug subscription then it will be treated as a {@code always-disabled} flag and no debug values will be stored for it.
+ *
+ * This event is fired once per tick during {@linkplain ClientDebugSubscriber#requestedSubscriptions()}.
+ *
+ * This event is fired on the {@linkplain LogicalSide#CLIENT logical client}.
+ *
+ * @apiNote Each {@linkplain DebugSubscription debug subscription} should only be registered once.
+ */
+public final class AddDebugSubscriptionFlagsEvent extends Event {
+ private final ObjectBooleanBiConsumer> registrar;
+
+ @ApiStatus.Internal
+ public AddDebugSubscriptionFlagsEvent(ObjectBooleanBiConsumer> registrar) {
+ this.registrar = registrar;
+ }
+
+ /**
+ * Register a new flag for the given {@linkplain DebugSubscription debug subscription}.
+ *
+ * @param subscription {@linkplain DebugSubscription debug subscription} to register flag for.
+ * @param flag Flag used to conditionally enable or disable the given {@linkplain DebugSubscription debug subscription}.
+ */
+ public void addFlag(DebugSubscription> subscription, boolean flag) {
+ registrar.accept(subscription, flag);
+ }
+
+ /**
+ * Register a new flag for the given {@linkplain DebugSubscription debug subscription}.
+ *
+ * @param subscription {@linkplain DebugSubscription Debug subscription} to register flag for.
+ * @param flag Flag used to conditionally enable or disable the given {@linkplain DebugSubscription debug subscription}.
+ */
+ public void addFlag(Supplier extends DebugSubscription>> subscription, boolean flag) {
+ addFlag(subscription.get(), flag);
+ }
+
+ /**
+ * Mark the given {@linkplain DebugSubscription debug subscription} as only active in dev.
+ *
+ * Using this method will mark your {@linkplain DebugSubscription debug subscription} as only being enabled in dev a.k.a when {@linkplain FMLEnvironment#isProduction()} == {@code false}.
+ *
+ * @param subscription {@linkplain DebugSubscription debug subscription} to register flag for.
+ */
+ public void addActiveInDev(DebugSubscription> subscription) {
+ addFlag(subscription, !FMLEnvironment.isProduction());
+ }
+
+ /**
+ * Mark the given {@linkplain DebugSubscription debug subscription} as only active in dev.
+ *
+ * Using this method will mark your {@linkplain DebugSubscription debug subscription} as only being enabled in dev a.k.a when {@linkplain FMLEnvironment#isProduction()} == {@code false}.
+ *
+ * @param subscription {@linkplain DebugSubscription Debug subscription} to register flag for.
+ */
+ public void addActiveInDev(Supplier extends DebugSubscription>> subscription) {
+ addActiveInDev(subscription.get());
+ }
+}
diff --git a/src/client/java/net/neoforged/neoforge/client/event/RegisterDebugRenderersEvent.java b/src/client/java/net/neoforged/neoforge/client/event/RegisterDebugRenderersEvent.java
new file mode 100644
index 0000000000..ee2d87358c
--- /dev/null
+++ b/src/client/java/net/neoforged/neoforge/client/event/RegisterDebugRenderersEvent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.neoforged.neoforge.client.event;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.debug.DebugRenderer;
+import net.neoforged.bus.api.Event;
+import net.neoforged.fml.LogicalSide;
+import net.neoforged.fml.event.IModBusEvent;
+import org.jetbrains.annotations.ApiStatus;
+
+/**
+ * This event allows mods to register custom {@link DebugRenderer.SimpleDebugRenderer}s.
+ *
+ * This event is fired during {@link DebugRenderer#refreshRendererList()}.
+ *
+ * This event is fired on the mod-specific event bus, only on the {@linkplain LogicalSide#CLIENT logical client}.
+ */
+public final class RegisterDebugRenderersEvent extends Event implements IModBusEvent {
+ private final Consumer> registrar;
+
+ @ApiStatus.Internal
+ public RegisterDebugRenderersEvent(Consumer> registrar) {
+ this.registrar = registrar;
+ }
+
+ /**
+ * Registers the given debug renderer
+ *
+ * @param factory Factory used to construct the debug renderer
+ */
+ public void register(Function factory) {
+ registrar.accept(factory);
+ }
+
+ /**
+ * Registers the given debug renderer
+ *
+ * @param renderer Debug renderer to be registered
+ */
+ public void register(DebugRenderer.SimpleDebugRenderer renderer) {
+ register(client -> renderer);
+ }
+}
diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/CustomDebugSubscriberTest.java b/tests/src/main/java/net/neoforged/neoforge/debug/CustomDebugSubscriberTest.java
new file mode 100644
index 0000000000..29184b01b0
--- /dev/null
+++ b/tests/src/main/java/net/neoforged/neoforge/debug/CustomDebugSubscriberTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) NeoForged and contributors
+ * SPDX-License-Identifier: LGPL-2.1-only
+ */
+
+package net.neoforged.neoforge.debug;
+
+import com.mojang.serialization.MapCodec;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.culling.Frustum;
+import net.minecraft.client.renderer.debug.DebugRenderer;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.gizmos.Gizmos;
+import net.minecraft.network.codec.ByteBufCodecs;
+import net.minecraft.resources.Identifier;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.util.CommonColors;
+import net.minecraft.util.debug.DebugSubscription;
+import net.minecraft.util.debug.DebugValueAccess;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.BaseEntityBlock;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.BlockEntityType;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.storage.ValueInput;
+import net.minecraft.world.level.storage.ValueOutput;
+import net.minecraft.world.phys.BlockHitResult;
+import net.neoforged.api.distmarker.Dist;
+import net.neoforged.neoforge.client.event.AddDebugSubscriptionFlagsEvent;
+import net.neoforged.neoforge.client.event.RegisterDebugRenderersEvent;
+import net.neoforged.neoforge.common.NeoForge;
+import net.neoforged.neoforge.registries.DeferredHolder;
+import net.neoforged.testframework.DynamicTest;
+import net.neoforged.testframework.annotation.ForEachTest;
+import net.neoforged.testframework.annotation.TestHolder;
+import net.neoforged.testframework.registration.RegistrationHelper;
+
+@ForEachTest(groups = "custom_debug_subscribers", side = Dist.CLIENT)
+public interface CustomDebugSubscriberTest {
+ @TestHolder(description = "Renders debug info for a new block entity using debug renderers and subscribers", enabledByDefault = true)
+ static void testDebugSubscriptions(DynamicTest test, RegistrationHelper reg) {
+ var id = "debug_subscription";
+ var registryName = Identifier.fromNamespaceAndPath(reg.modId(), id);
+ var blockEntityType = DeferredHolder.create(Registries.BLOCK_ENTITY_TYPE, registryName);
+
+ // register our custom debug subscription
+ var debugSubscription = reg.registrar(Registries.DEBUG_SUBSCRIPTION).register(id, () -> new DebugSubscription<>(ByteBufCodecs.VAR_INT, 200));
+
+ final class DebugBlockEntity extends BlockEntity {
+ private int counter = 0;
+
+ private DebugBlockEntity(BlockPos pos, BlockState blockState) {
+ super(blockEntityType.value(), pos, blockState);
+ }
+
+ public void incrementCounter() {
+ counter++;
+ setChanged();
+ }
+
+ @Override
+ protected void saveAdditional(ValueOutput output) {
+ super.saveAdditional(output);
+ output.putInt("counter", counter);
+ }
+
+ @Override
+ protected void loadAdditional(ValueInput input) {
+ super.loadAdditional(input);
+ counter = input.getIntOr("counter", 0);
+ }
+
+ @Override
+ public void registerDebugValues(ServerLevel level, Registration registration) {
+ // mark this entity for displaying debug info for our subscription
+ registration.register(debugSubscription.value(), () -> counter);
+ }
+ }
+
+ // generic block for our block entity
+ final class DebugBlock extends BaseEntityBlock {
+ private DebugBlock(Properties properties) {
+ super(properties);
+ }
+
+ @Override
+ protected MapCodec extends BaseEntityBlock> codec() {
+ return null;
+ }
+
+ @Override
+ protected InteractionResult useWithoutItem(BlockState blockState, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
+ if (!(level.getBlockEntity(pos) instanceof DebugBlockEntity blockEntity)) {
+ return InteractionResult.FAIL;
+ }
+
+ blockEntity.incrementCounter();
+ return InteractionResult.SUCCESS;
+ }
+
+ @Override
+ public BlockEntity newBlockEntity(BlockPos pos, BlockState blockState) {
+ return new DebugBlockEntity(pos, blockState);
+ }
+ }
+
+ var block = reg.blocks().registerBlock(id, DebugBlock::new);
+ reg.items().registerSimpleBlockItem(block);
+ reg.registrar(Registries.BLOCK_ENTITY_TYPE).register(id, () -> new BlockEntityType<>(DebugBlockEntity::new, block.value()));
+
+ // debug renderer used to render counters above our block entities
+ final class DebugCountRenderer implements DebugRenderer.SimpleDebugRenderer {
+ private DebugCountRenderer(Minecraft client) {}
+
+ @Override
+ public void emitGizmos(double camX, double camY, double camZ, DebugValueAccess valueAccess, Frustum frustum, float partialTicks) {
+ // list is populated via 'DebugBlockEntity.registerDebugValues'
+ // while we are using block entities this, you can also register subscribers for entities and chunks too
+ valueAccess.forEachBlock(debugSubscription.value(), (pos, count) -> {
+ Gizmos.billboardTextOverBlock(count + "", pos, 0, CommonColors.WHITE, .5F);
+ });
+ }
+ }
+
+ // mark our subscriber as only being active in dev
+ NeoForge.EVENT_BUS.addListener(AddDebugSubscriptionFlagsEvent.class, event -> event.addActiveInDev(debugSubscription.value()));
+
+ // register our debug renderer
+ reg.eventListeners().accept((RegisterDebugRenderersEvent event) -> event.register(DebugCountRenderer::new));
+
+ test.pass();
+ }
+}