Skip to content

Bug Report, Analysis and Proposed Solutions Regarding Missing GD.Print/Debug.WriteLine/Debug.Assert Output in Visual Studio #108965

@opadin

Description

@opadin

Tested versions

  • Reproducible since years in all versions of Godot concerning the Windows platform and Visual Studio development

System information

Godot v4.4.1.stable.mono - Windows 11 (build 26100) - Multi-window, 1 monitor - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 4070 (NVIDIA; 32.0.15.7688) - 13th Gen Intel(R) Core(TM) i7-13700KF (24 threads)

Issue description

Following Issues are handled here:

  • GD.Print() used in C# does not output to Debug-Output-Pane in Visual Studio
  • Debug.WriteLine used in C# does not output to Debug-Output-Pane in Visual Studio
  • Trace.WriteLine used in C# does not output to Debug-Output-Pane in Visual Studio
  • Console.WriteLine used in C# does not output to Debug-Output-Pane in Visual Studio
  • Debug.Assert used in C# does not stop, when triggered and information does not output to Debug-Output-Pane in Visual Studio

See also: godotengine/godot-proposals#8648

For quite some time now, it's no longer easily possible to display information via the Output pane in Visual Studio when working with Godot projects in C#. This is significantly detrimental to professional development with Godot in the long term. This week, I sat down (4 days, compiling godot thirty times, heavy testing) to analyze the issue and was able to identify the following points (it would be great if other developers could confirm or disprove these findings — thank you!)

And thanks to the Godot Team, providing not only a feature-rich and -growing engine, but also very maintainable source code. A deep bow to everyone involved.

Why are GD.Print outputs no longer shown in Visual Studio’s Output pane while debugging?

GD.Print output traverses the following function chain from C# to C++:

file function call
modules\mono\glue\runtime_interop.cpp godotsharp_print() print_line()
core\string\print_string.h print_line() __print_line()
core\string\print_string.cpp __print_line() OS::get_singleton()->print()
core\os\os.cpp OS::print() _logger->logv()
core\io\logger.cpp CompositeLogger::logv() loggers[i]->logv()
platform\windows\windows_terminal_logger.cpp WindowsTerminalLogger::logv() WriteFile()

The problem occurs at the end of the chain — when writing via WriteFile(). The handle, retrieved via GetStdHandle(STD_OUTPUT_HANDLE), is NULL. Importantly, the return value is not INVALID_HANDLE_VALUE (which would indicate an error), but simply NULL. This means the application does not have console output handles — nor has it redirected them.

In the constructor OS_Windows::OS_Windows(), if it's not a console application (like the “editor” target), the function RedirectIOToConsole() attempts a redirection. However, AttachConsole(ATTACH_PARENT_PROCESS) fails, and thus redirection does not occur.

To understand why AttachConsole() fails, one must look into Visual Studio, .NET Core, and its evolution. Since the move to .NET Standard and the transition from VS 2017 to 2019, Microsoft has made changes to debug output handling. Previously, GUI applications (using WinMain and /SUBSYSTEM:WINDOWS) could still output to a redirected console during debugging. Today, this no longer works the same way. Since the parent process (Visual Studio) does not provide a console, child processes can't inherit one via AttachConsole(ATTACH_PARENT_PROCESS).

Note: Of course, everything works fine if you start the editor manually via a cmd window with --path and --verbose. Here, the parent process has a console, which the child can use.

In Visual Studio the absence of the Godot verbose outputs on application exit (e.g., orphan warnings, leaks, exceptions) is particularly problematic.

Why do alternatives like Debug.WriteLine, Console.WriteLine, or Trace.WriteLine not work — and especially: why doesn’t Debug.Assert(expression) work?

Console.WriteLine

Console.WriteLine() doesn’t work for the same reasons as above: there is no console.

Debug.WriteLine (and Trace.WriteLine)

Visual Studio’s Output pane accepts debug output via OutputDebugString (C). In C#, this is typically accessed through Debug.WriteLine() or Trace.WriteLine(). These don’t work either. Why?

Both functions rely on the list of installed TraceListeners. Normally, .NET adds the DefaultTraceListener, which routes debug and trace output to OutputDebugString(message) or Debugger.Log().

However, the Godot team added their own TraceListener about 6 years ago, intended to redirect C# traces and assertions into the Godot Editor's internal log view. Unfortunately, this custom GodotTraceListener uses GD.Print(!) to output information — and, crucially, it clears the entire list of listeners instead of just appending itself. As a result, the DefaultTraceListener is lost — and so is all standard debug output.

Debug.Assert

Debug.Assert() fails for the same reason: without the DefaultTraceListener, assertions are not processed properly (at least the output is displayed in the godot editor log pane). This is a major loss, since Debug.Assert() is a key tool to check assumptions during development.

What are the options to solve the problem?

Workarounds without modifying Godot source code

Seeing Console.WriteLine and GD.Print output

1. AllocConsole

Since AttachConsole() fails, one could allocate a new console using AllocConsole(). This works, but has downsides:

  • A separate console window opens during debugging.

  • Output does not appear in the Output pane.

  • The console closes when the application exits, potentially losing valuable last-minute messages.

    namespace ConsoleHelper
    {
        internal static class NativeMethods
        {
            [DllImport("kernel32.dll", SetLastError = true)]
            internal static extern int AllocConsole();
        }
    }
    
    public partial class Class : Node {
        public override void _Ready()
        {
            ConsoleHelper.NativeMethods.AllocConsole();
        }
    }

2. Using the console stub

Godot provides a stub *.console.exe which invokes the main executable. However: This is just a wrapper, and child-process debugging (e.g., breakpoints) does not work well. Output is still not visible in Visual Studio’s Output pane.

3. Building your own console-enabled Godot editor

If you build Godot yourself, you can configure it as a proper console app:
scons platform=windows target=editor module_mono_enabled=yes windows_subsystem=console
Also make sure to enable Native Code Debugging in the project settings. Breakpoints are working but output still doesn't go to Debug-Pane.

Re-add the DefaultTraceListener

You can manually restore the DefaultTraceListener:

public partial class Class : Node {
    ...
    public override void _Ready()
    {
        ...
        System.Diagnostics.Trace.Listeners.Add(new DefaultTraceListener());
        ...
    }
    ...
}

Combine both ideas in an autoload class

Declare this as an autoload singleton (via Project Settings > AutoLoad):

public partial class CSharpDebugHelper : Node
{
    public static CSharpDebugHelper Instance { get; private set; } = null!;

    public override void _Ready()
    {
#if DEBUG
        if (!Trace.Listeners.OfType<DefaultTraceListener>().Any())
        {
            Trace.Listeners.Add(new DefaultTraceListener());
        }
        ConsoleHelper.NativeMethods.AllocConsole();
#endif
        Instance = this;
    }
}

Proper Fixes via Code Modification

IMPORTANT NOTE: All following changes are for the code in version 4.4.1. Should be easy adaptable to latest version, because the critical files are not changed much. If desired, I can prepare diffs for a special version.

The above-mentioned workarounds are all surface-level. What we really want is:

  • GD.Print() output to appear inside the Output pane
  • Persistent support for Debug.WriteLine() and Debug.Assert() in all projects

GD.Print

There is a related PR here: Windows - display every log message in Visual Studio Output window, which proposes using OutputDebugString(). However, it applies this inside WindowsTerminalLogger — which is probably not the right place. Because the WindowsTerminalLogger is intended for CONSOLE output during development debugging AND on end-user systems (for Godots verbose logging information). Also, this patch removes proper error handling.

Instead, Godot should add a PrintHandler, similar to the existing ErrorHandler, and install it conditionally:

--- a/platform/windows/os_windows.h
+++ b/platform/windows/os_windows.h
@@ -115,6 +115,7 @@ class OS_Windows : public OS {
        CrashHandler crash_handler;

 #ifdef WINDOWS_DEBUG_OUTPUT_ENABLED
+       PrintHandlerList print_handlers;
        ErrorHandlerList error_handlers;
 #endif
--- a/platform/windows/os_windows.cpp
+++ b/platform/windows/os_windows.cpp
@@ -242,6 +242,19 @@ void OS_Windows::initialize_debugging() {
}

#ifdef WINDOWS_DEBUG_OUTPUT_ENABLED
+static void _print_handler(void *p_this, const String &p_string, bool p_error, bool p_rich) {
+
+       const char *prefix = "";
+       const char *suffix = "\n";
+       if (p_error) {
+               prefix = " ERROR: ";
+       } else if (p_rich) {
+               prefix = " RICH: ";
+       }
+       String p_str = prefix + p_string + suffix;
+       OutputDebugStringW((LPCWSTR)p_str.utf16().ptr());
+}
+
static void _error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) {
       String err_str;
       if (p_errorexp && p_errorexp[0]) {
@@ -258,6 +271,10 @@ void OS_Windows::initialize() {
       crash_handler.initialize();

#ifdef WINDOWS_DEBUG_OUTPUT_ENABLED
+       print_handlers.printfunc = _print_handler;
+       print_handlers.userdata = this;
+       add_print_handler(&print_handlers);
+
       error_handlers.errfunc = _error_handler;
       error_handlers.userdata = this;
       add_error_handler(&error_handlers);

This would mirror how error output is handled and preserve the current structure. WindowsTerminalLogger remains untouched for actual console output during develop/debug AND also for customers on production systems.

Debug.WriteLine and Debug.Assert

At first, I thought it would be sufficient not to clear the Trace.Listeners list, so that both the DefaultTraceListener and the GodotTraceListener would remain active. However, this leads to duplicate output in the debug pane, which becomes quite cluttered. In the end, though, none of this is actually necessary:

Debug.WriteLine now works again because GD.Print works. So there's no additional effort needed here.

Debug.Assert now partially works. The GD.Print output does appear in the debug pane. However, the debugger does not pause at the point where the Assert fails. This still requires a small change to the GodotTraceListener.

--- a/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotTraceListener.cs
+++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/GodotTraceListener.cs
@@ -14,6 +14,7 @@ namespace Godot
             GD.Print(message);
         }

+        [DebuggerStepThrough]
         public override void Fail(string message, string detailMessage)
         {
             GD.PrintErr("Assertion failed: ", message);
@@ -28,6 +29,11 @@ namespace Godot
             {
                 // ignored
             }
+
+            if (Debugger.IsAttached)
+            {
+                Debugger.Break();
+            }
         }
     }
 }

Note: Do not forget, that Debug.Write() writes - with GD.PrintRaw - ONLY and direct to the OS-Logger, skipping the print-handlers (even so for Godot Editor Nodes) ... and can not be redirected to the Debug-Pane.

However, there is one caveat:

We need to enable Mixed Code Debugging (setting in the launch profiles), which means that the debug output will only be written correctly after the .NET module has been loaded. As a result, early informational messages (e.g., WorkerThreadPool, TextServer, VULKAN, Shader, WASAPI, etc.) are not shown.

Results:

Image Image

Steps to reproduce

self describing

Minimal reproduction project (MRP)


Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions