From ae75fc7e8b31063608dbb56d374e8b3ccf4bd8b3 Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 29 Jan 2020 13:45:16 +0000 Subject: [PATCH 1/7] Debugger: Basic design and usage documentation. --- docs/debugger.md | 183 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 docs/debugger.md diff --git a/docs/debugger.md b/docs/debugger.md new file mode 100644 index 000000000..2cb69ebc0 --- /dev/null +++ b/docs/debugger.md @@ -0,0 +1,183 @@ +# Debugger Testing + +This document describes Amber's shader debugger testing framework, which allows developers to write tests for Vulkan drivers that expose shader debugging functionality via the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/). + +--- +**Work In Progress** + +Note that shader debugging is very much work-in-progress: + +* Vulkan shader debugging currently does not have a formalized specification. A shader debugger implementation is being developed in [SwiftShader](https://swiftshader.googlesource.com/SwiftShader/), which one day may become a reference implementation for a formal specifiction. +* Currently SwiftShader is the only Vulkan driver to implement a [DAP based shader debugger](https://swiftshader.googlesource.com/SwiftShader/+/refs/heads/master/docs/VulkanShaderDebugging.md). This implementation is also work-in-progress, and may significantly change. +* Currently the debugger connection uses localhost sockets to connect Amber to the driver. The `VK_DEBUGGER_PORT` environment variable must be set to an unused localhost port number before attempting to run any Amber scripts that use the `DEBUG` command. +* [`OpenCL.DebugInfo.100`](https://www.khronos.org/registry/spir-v/specs/unified1/OpenCL.DebugInfo.100.mobile.html) is a SPIR-V extended instruction set that adds rich debug information to the shader program, allowing for high-level shader source debugging. `OpenCL.DebugInfo.100` is not currently generated by [DXC](https://github.com/microsoft/DirectXShaderCompiler) or [glslang](https://github.com/KhronosGroup/glslang), but initial work has started to try and add `OpenCL.DebugInfo.100` support to DXC. +* `OpenCL.DebugInfo.100` insstructions are not currently preserved by many of the [SPIR-V Tools](https://github.com/KhronosGroup/SPIRV-Tools) optimization passes, so these optimizations should not currently be used. +* `OpenCL.DebugInfo.100` may be incorrectly interpreted by the [SPIR-V Tools](https://github.com/KhronosGroup/SPIRV-Tools) validator, so Amber should currently be invoked with the `--disable-spirv-val` flag. + +--- + +## Usage + +### Building + +The debugger testing functionality is disabled by default, and has to be enabled with the `AMBER_ENABLE_VK_DEBUGGING` CMake flag. + +As SwiftShader is currently the only Vulkan driver that supports DAP-based shader debugging, you will also likely want to build SwiftShader as part of Amber, using the `AMBER_ENABLE_SWIFTSHADER` flag. + +Both of these can be set by running CMake with: + +``` +cmake -DAMBER_ENABLE_SWIFTSHADER=1 -DAMBER_ENABLE_VK_DEBUGGING=1 +``` + +### AmberScript + +Debugger tests must be written in AmberScript. + +The `DEBUG` command extends the [`RUN` command](amber_script.md#Run-a-pipeline) to execute a draw or compute shader. +`DEBUG` expects exactly the same arguments to follow as `RUN` ([see: 'Run a pipeline'](amber_script.md#Run-a-pipeline)). However, unlike `RUN`, `DEBUG` begins a block that must be terminated with an `END`. + +Within this `RUN` block, you may declare any number of `THREAD` command blocks that list a sequence of debugger commands to perform on a single compute, vertex or fragment shader invocation: + +* `THREAD GLOBAL_INVOCATION_ID` \ \ \ + + Declares a sequence of debugger commands to run when the compute shader invocation with the given global invocation identifier is executed. + +* `THREAD VERTEX_INDEX` \ + + Declares a sequence of debugger commands to run when the shader invocation for the vertex with the given index is executed. + +* `THREAD FRAGMENT_WINDOW_SPACE_POSITION` \ \ + + Defines a sequence of debugger commands to run when the shader invocation for the fragment with the given window space position is executed. + + TODO(bclayton): This has not yet been implemented. + +Each of the `THREAD` commands begins a block that must be terminated with an `END`. + +Within each `THREAD` command block, you may use any of the following commands to control execution of the shader, and to verify the debugger's behaviour: + +* `STEP_IN` + + Single line step execution of the thread, stepping into any function calls. + +* `STEP_OVER` + + Single line step execution of the thread, stepping over any function calls. + +* `STEP_OUT` + + Run, without stopping to the end of the currently executing function. If the current function is not the top most of the call stack, then the debugger will pause at the next line after the function call. + +* `EXPECT LOCATION` \ \ [\] + + Verifies that the debugger is currently paused at the given line location. + The [\] is an additional, optional check that verifies the line of the file reported by the debuggeer is as expected. + +* `EXPECT LOCAL` \ `EQ` [\] + + Verifies that the local variable with the given name holds the expected value. \ may contain `.` delimiters to index structure or array types. + +Every shader invocation covered by a `THREAD` block must be executed. +It is a test failure if the debugger does not break at all the `THREAD` shader invocations declared in the `DEBUG` block. + +#### Example: + +Given the following HLSL vertex shader: + +```hlsl +/* 1 */ // simple_vs.hlsl +/* 2 */ struct VS_OUTPUT { +/* 3 */ float4 pos : SV_POSITION; +/* 4 */ float4 color : COLOR; +/* 5 */ }; +/* 6 */ +/* 7 */ VS_OUTPUT main(float4 pos : POSITION, +/* 8 */ float4 color : COLOR) { +/* 9 */ VS_OUTPUT vout; +/* 10 */ vout.pos = pos; +/* 11 */ vout.color = color; +/* 12 */ return vout; +/* 13 */ } +``` + +The following performs a basic debugger test for the 3rd vertex in the triangle list: + +```groovy +DEBUG pipeline DRAW_ARRAY AS TRIANGLE_LIST START_IDX 0 COUNT 6 + THREAD VERTEX_INDEX 2 + // Debugger starts at line 9. Inspect input variables. + EXPECT LOCATION "simple_vs.hlsl" 9 " VS_OUTPUT vout;" + EXPECT LOCAL "pos.x" EQ -1.007874 + EXPECT LOCAL "pos.y" EQ 1.000000 + EXPECT LOCAL "pos.z" EQ 0.000000 + EXPECT LOCAL "color.x" EQ 1.000000 + EXPECT LOCAL "color.y" EQ 0.000000 + EXPECT LOCAL "color.z" EQ 0.000000 + // Step to line 10. + STEP_IN + EXPECT LOCATION "simple_vs.hlsl" 10 " vout.pos = pos;" + // Step to line 11 and read result of line 10. + STEP_IN + EXPECT LOCAL "vout.pos.x" EQ -1.007874 + EXPECT LOCAL "vout.pos.y" EQ 1.000000 + EXPECT LOCAL "vout.pos.z" EQ 0.000000 + EXPECT LOCATION "simple_vs.hlsl" 11 " vout.color = color;" + // Step to line 12 and read result of line 11. + STEP_IN + EXPECT LOCAL "vout.color.x" EQ 1.000000 + EXPECT LOCAL "vout.color.y" EQ 0.000000 + EXPECT LOCAL "vout.color.z" EQ 0.000000 + EXPECT LOCATION "simple_vs.hlsl" 12 " return vout;" + CONTINUE + END +END +``` + +## Implementation + +This section covers the design of how the debugger testing is implemented in Amber. + +### Parsing + +`Parser::ParseDebug()` starts by immediately calling `ParseRun()`, which parses the tokens that normally immediately follow a `RUN` command. On successful parse, `ParseRun()` appends a command into the `command_list_` vector. \ +`ParseDebug()` then constructs a new `amber::debug::Script`, and parses the debugger command block. The `amber::debug::Script` is then assigned to the command at the back of the `command_list_` (added by `ParseRun()`). + +### `amber::debug::Script` + +`amber::debug::Script` implements the `amber::debug::Events` interface, and records the calls made on it. +These calls can be replayed to another `amber::debug::Events`, using the `amber::debug::Script::Run(Events*)` method. + +### `amber::debug::Events` + +The `amber::debug::Events` represents the `THREAD` commands in the Amber script, providing methods that set breakpoints for particular shader invocations. + +`amber::debug::Events` is the interface implemented by `amber::debug::Script` for recording the parsed debugger script, and extended by the `amber::Engine::Debugger` interface, used to actually drive an engine debugger. + +The `amber::debug::Events` interface has a number of `BreakOn`XXX`()` methods that have parameters for the shader invocation of interest (global invocation ID, vertex index, etc) and a `OnThread` function callback parameter. + +The `OnThread` callback has the signature `void(amber::debug::Thread*)` which is called to control the debugger thread of execution and perform test verifications. + +### `amber::debug::Thread` + +`amber::debug::Thread` is the interface used to control a debugger thread of execution, and represents the script commands within the `THREAD` script blocks. + +The following implementations of the `amber::debug::Thread` interface are returned by the `amber::debug::Script::BreakOn`XXX`()` methods: +* `amber::debug::Script` implements this interface to record the `THREAD` commands in the Amber script, which will be replayed when calling `amber::debug::Script::Run(amber::debug::Events*)`. +* `amber::Engine::Debugger` implementations will implement the `amber::debug::Thread` interface to actually control the debugger thread of execution and perform test verifications. + +### `amber::Engine::Debugger` + +The `amber::Engine::Debugger` interface extends the `amber::debug::Events` interface with a single `Flush()` method that ensures that all the previous event have been executed. `Flush()` returns a `amber::Result`, which holds the results of all the `amber::debug::Thread::Expect`XXX`()` calls. + +The `amber::Engine::Debugger` interface can be obtained from the `amber::Engine` using the `amber::Engine::GetDebugger()` method. + +### Debugger Execution + +`amber::Executor::Execute()` drives the `amber::Engine::Debugger`: + +* The before executing the first `amber::Command` that holds a `amber::debug::Script`, a `amber::Engine::Debugger` is obtained from the `amber::Engine`, and `amber::Engine::Debugger::Connect()` is called to create a connection to the Vulkan shader debugger. +* If the `amber::Command` holds a `amber::debug::Script`, then this script is executed on the `amber::Engine::Debugger` using the `amber::debug::Script::Run(amber::debug::Events *)` method, before the Vulkan command is executed. +* The command is then executed with `amber::Executor::ExecuteCommand()` +* Once the command has completed, all debugger threads are synchronized and debugger test results are collected with a call to `amber::Engine::Debugger::Flush()`. +* This process is repeated for all commands in the script. \ No newline at end of file From 3581e82d39cbe66b07de2289127d371d05eb8319 Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 29 Jan 2020 13:43:35 +0000 Subject: [PATCH 2/7] Debugger: Add amber::debug namespace This namespace holds the types used to describe the tests to perform on a shader debugger. `debug.cc` implements the `amber::debug::Script` class, which is a simple record-and-replay implementation of the debugger interfaces. This is used to hold the parsed debugger instructions, and can be replayed onto the real debugger when the commands are executed. The parser for generating the debug scripts along with the debugger itself come in later changes. --- Android.mk | 1 + src/CMakeLists.txt | 1 + src/debug.cc | 115 +++++++++++++++++++++++++++++++++++++++++ src/debug.h | 125 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+) create mode 100644 src/debug.cc create mode 100644 src/debug.h diff --git a/Android.mk b/Android.mk index 81bdd11ea..2e918d07b 100644 --- a/Android.mk +++ b/Android.mk @@ -27,6 +27,7 @@ LOCAL_SRC_FILES:= \ src/buffer.cc \ src/command.cc \ src/command_data.cc \ + src/debug.cc \ src/descriptor_set_and_binding_parser.cc \ src/engine.cc \ src/executor.cc \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ae937f62d..a8a43a26f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ set(AMBER_SOURCES buffer.cc command.cc command_data.cc + debug.cc descriptor_set_and_binding_parser.cc engine.cc executor.cc diff --git a/src/debug.cc b/src/debug.cc new file mode 100644 index 000000000..7d6c2265f --- /dev/null +++ b/src/debug.cc @@ -0,0 +1,115 @@ +// Copyright 2020 The Amber Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/debug.h" + +#include +#include + +#include "src/make_unique.h" + +namespace amber { +namespace debug { + +namespace { + +// ThreadScript is an implementation of amber::debug::Thread that records all +// calls made on it, which can be later replayed using ThreadScript::Run(). +class ThreadScript : public Thread { + public: + void Run(Thread* thread) { + for (auto f : sequence_) { + f(thread); + } + } + // Thread compliance + void StepOver() override { + sequence_.emplace_back([](Thread* t) { t->StepOver(); }); + } + + void StepIn() override { + sequence_.emplace_back([](Thread* t) { t->StepIn(); }); + } + + void StepOut() override { + sequence_.emplace_back([](Thread* t) { t->StepOut(); }); + } + + void Continue() override { + sequence_.emplace_back([](Thread* t) { t->Continue(); }); + } + + void ExpectLocation(const Location& location, + const std::string& line) override { + sequence_.emplace_back( + [=](Thread* t) { t->ExpectLocation(location, line); }); + } + + void ExpectLocal(const std::string& name, int64_t value) override { + sequence_.emplace_back([=](Thread* t) { t->ExpectLocal(name, value); }); + } + + void ExpectLocal(const std::string& name, double value) override { + sequence_.emplace_back([=](Thread* t) { t->ExpectLocal(name, value); }); + } + + void ExpectLocal(const std::string& name, const std::string& value) override { + sequence_.emplace_back([=](Thread* t) { t->ExpectLocal(name, value); }); + } + + private: + using Event = std::function; + std::vector sequence_; +}; + +} // namespace + +Thread::~Thread() = default; +Events::~Events() = default; + +void Script::Run(Events* e) { + for (auto f : sequence_) { + f(e); + } +} + +void Script::BreakOnComputeGlobalInvocation(uint32_t x, + uint32_t y, + uint32_t z, + const OnThread& callback) { + auto script = std::make_shared(); + callback(script.get()); // Record + + sequence_.emplace_back([=](Events* events) { + events->BreakOnComputeGlobalInvocation(x, y, z, [=](Thread* thread) { + script->Run(thread); // Replay + }); + }); +} + +void Script::BreakOnVertexIndex(uint32_t index, const OnThread& callback) { + // std::make_shared is used here instead of MakeUnique as std::function is + // copyable, and cannot capture move-only values. + auto script = std::make_shared(); + callback(script.get()); // Record + + sequence_.emplace_back([=](Events* events) { + events->BreakOnVertexIndex(index, [=](Thread* thread) { + script->Run(thread); // Replay + }); + }); +} + +} // namespace debug +} // namespace amber diff --git a/src/debug.h b/src/debug.h new file mode 100644 index 000000000..0fe7b7cc2 --- /dev/null +++ b/src/debug.h @@ -0,0 +1,125 @@ +// Copyright 2020 The Amber Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef SRC_DEBUG_H_ +#define SRC_DEBUG_H_ + +#include + +#include +#include +#include + +#include "amber/result.h" + +/// amber::debug holds the types used for testing a graphics debugger. +namespace amber { +namespace debug { + +/// Location holds a file path and a 1-based line number. +struct Location { + std::string file; + uint32_t line; +}; + +/// Thread is the interface used to control a single debugger thread of +/// execution. +class Thread { + public: + virtual ~Thread(); + + /// StepOver instructs the debugger to perform a single line step on the given + /// thread of execution, stepping over any function call instructions. + virtual void StepOver() = 0; + + /// StepIn instructs the debugger to perform a single line step on the given + /// thread of execution, stepping into any function call instructions. + virtual void StepIn() = 0; + + /// StepOut instructs the debugger to resume execution of the given thread of + /// execution. If the current function is not the top most of the call stack, + /// then the debugger will pause at the next line after the call to the + /// current function. + virtual void StepOut() = 0; + + /// Continue instructs the debugger to resume execution of the given thread of + /// execution. + virtual void Continue() = 0; + + /// ExpectLocation verifies that the debugger is currently suspended for the + /// given thread of execution at the specified source location. If |line| is + /// non-empty, then the line's textual source will also be verified. + virtual void ExpectLocation(const Location& location, + const std::string& line) = 0; + + /// ExpectLocal verifies that the local variable with the given name has the + /// expected value. |name| may contain `.` delimiters to index structure or + /// array types. + virtual void ExpectLocal(const std::string& name, int64_t value) = 0; + virtual void ExpectLocal(const std::string& name, double value) = 0; + virtual void ExpectLocal(const std::string& name, + const std::string& value) = 0; +}; + +/// Events is the interface used to control the debugger. +class Events { + public: + using OnThread = std::function; + + virtual ~Events(); + + /// BreakOnComputeGlobalInvocation instructs the debugger to set a breakpoint + /// at the start of the compute shader program for the invocation with the + /// global invocation identifier [|x|, |y|, |z|]. The |amber::debug::Thread|* + /// parameter to the |OnThread| callback is used to control and verify the + /// debugger behavior for the given thread. + virtual void BreakOnComputeGlobalInvocation(uint32_t x, + uint32_t y, + uint32_t z, + const OnThread&) = 0; + + /// BreakOnVertexIndex instructs the debugger to set a breakpoint at the start + /// of the vertex shader program for the invocation with the vertex index + /// [index]. The |amber::debug::Thread|* parameter to the |OnThread| callback + /// is used to control and verify the debugger behavior for the given thread. + virtual void BreakOnVertexIndex(uint32_t index, const OnThread&) = 0; +}; + +/// Script is an implementation of the |amber::debug::Events| interface, and is +/// used to record all the calls made on it, which can be later replayed with +/// |Script::Run|. +class Script : public Events { + public: + /// Run replays all the calls made to the |Script| on the given |Events| + /// parameter, including calls made to any |amber::debug::Thread|s passed to + /// |OnThread| callbacks. + void Run(Events*); + + // Events compliance + void BreakOnComputeGlobalInvocation(uint32_t x, + uint32_t y, + uint32_t z, + const OnThread&) override; + + void BreakOnVertexIndex(uint32_t index, const OnThread&) override; + + private: + using Event = std::function; + std::vector sequence_; +}; + +} // namespace debug +} // namespace amber + +#endif // SRC_DEBUG_H_ From de062aca548ed8fa52affaf632086b24ce75ca6a Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 29 Jan 2020 13:47:56 +0000 Subject: [PATCH 3/7] amberscript: Add parsing of debugger commands. The parsed `amber::debug::Script` is assigned to the `amber::Command`, but is currently unused. --- src/amberscript/parser.cc | 140 +++++++++++++++++++++++++++++++++++++- src/amberscript/parser.h | 3 + src/command.h | 10 +++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/amberscript/parser.cc b/src/amberscript/parser.cc index 5102d3871..7eb3b9647 100644 --- a/src/amberscript/parser.cc +++ b/src/amberscript/parser.cc @@ -242,7 +242,7 @@ Result Parser::Parse(const std::string& data) { bool Parser::IsRepeatable(const std::string& name) const { return name == "CLEAR" || name == "CLEAR_COLOR" || name == "COPY" || - name == "EXPECT" || name == "RUN"; + name == "EXPECT" || name == "RUN" || name == "DEBUG"; } // The given |name| must be one of the repeatable commands or this method @@ -258,6 +258,8 @@ Result Parser::ParseRepeatableCommand(const std::string& name) { return ParseExpect(); if (name == "RUN") return ParseRun(); + if (name == "DEBUG") + return ParseDebug(); return Result("invalid repeatable command: " + name); } @@ -1807,6 +1809,142 @@ Result Parser::ParseRun() { return Result("invalid token in RUN command: " + token->AsString()); } +Result Parser::ParseDebug() { + // DEBUG extends a RUN with debugger test cases + auto res = ParseRun(); + if (!res.IsSuccess()) { + return res; + } + + auto dbg = MakeUnique(); + for (auto token = tokenizer_->NextToken();; token = tokenizer_->NextToken()) { + if (token->IsEOL()) + continue; + if (token->IsEOS()) + return Result("missing DEBUG END command"); + if (token->IsIdentifier() && token->AsString() == "END") + break; + + if (token->AsString() == "THREAD") { + res = ParseDebugThread(dbg.get()); + if (!res.IsSuccess()) { + return res; + } + } else { + return Result("invalid token in DEBUG command: " + token->AsString()); + } + } + + command_list_.back()->SetDebugScript(std::move(dbg)); + + return Result(); +} + +Result Parser::ParseDebugThread(debug::Events* dbg) { + Result result; + auto parseThread = [&](debug::Thread* thread) { + result = ParseDebugThreadBody(thread); + }; + + auto token = tokenizer_->NextToken(); + if (token->AsString() == "GLOBAL_INVOCATION_ID") { + uint32_t invocation[3] = {}; + for (int i = 0; i < 3; i++) { + token = tokenizer_->NextToken(); + if (!token->IsInteger()) + return Result("expected invocation index"); + invocation[i] = token->AsUint32(); + } + dbg->BreakOnComputeGlobalInvocation(invocation[0], invocation[1], + invocation[2], parseThread); + } else if (token->AsString() == "VERTEX_INDEX") { + token = tokenizer_->NextToken(); + if (!token->IsInteger()) + return Result("expected vertex index"); + auto vertex_index = token->AsUint32(); + dbg->BreakOnVertexIndex(vertex_index, parseThread); + } else { + return Result("expected GLOBAL_INVOCATION_ID or VERTEX_INDEX"); + } + + return result; +} + +Result Parser::ParseDebugThreadBody(debug::Thread* thread) { + for (auto token = tokenizer_->NextToken();; token = tokenizer_->NextToken()) { + if (token->IsEOL()) { + continue; + } + if (token->IsEOS()) { + return Result("missing THREAD END command"); + } + if (token->IsIdentifier() && token->AsString() == "END") { + break; + } + + if (token->AsString() == "EXPECT") { + token = tokenizer_->NextToken(); + if (token->AsString() == "LOCATION") { + debug::Location location; + token = tokenizer_->NextToken(); + if (!token->IsString()) { + return Result("expected file name string"); + } + location.file = token->AsString(); + + token = tokenizer_->NextToken(); + if (!token->IsInteger()) { + return Result("expected line number"); + } + location.line = token->AsUint32(); + + std::string line_source; + token = tokenizer_->NextToken(); + if (token->IsString()) { + line_source = token->AsString(); + } + + thread->ExpectLocation(location, line_source); + + } else if (token->AsString() == "LOCAL") { + auto name = tokenizer_->NextToken(); + if (!name->IsString()) { + return Result("expected variable name"); + } + + if (tokenizer_->NextToken()->AsString() != "EQ") { + return Result("expected EQ"); + } + + auto value = tokenizer_->NextToken(); + if (value->IsHex() || value->IsInteger()) { + thread->ExpectLocal(name->AsString(), value->AsInt64()); + } else if (value->IsDouble()) { + thread->ExpectLocal(name->AsString(), value->AsDouble()); + } else if (value->IsString()) { + thread->ExpectLocal(name->AsString(), value->AsString()); + } else { + return Result("expected variable value"); + } + + } else { + return Result("expected LOCATION or LOCAL"); + } + } else if (token->AsString() == "STEP_IN") { + thread->StepIn(); + } else if (token->AsString() == "STEP_OUT") { + thread->StepOut(); + } else if (token->AsString() == "STEP_OVER") { + thread->StepOver(); + } else if (token->AsString() == "CONTINUE") { + thread->Continue(); + } else { + return Result("invalid token in THREAD block: " + token->AsString()); + } + } + return Result(); +} + Result Parser::ParseClear() { auto token = tokenizer_->NextToken(); if (!token->IsIdentifier()) diff --git a/src/amberscript/parser.h b/src/amberscript/parser.h index ef0eab3a8..eb40d1559 100644 --- a/src/amberscript/parser.h +++ b/src/amberscript/parser.h @@ -67,6 +67,9 @@ class Parser : public amber::Parser { Result ParsePipelineIndexData(Pipeline*); Result ParsePipelineSet(Pipeline*); Result ParseRun(); + Result ParseDebug(); + Result ParseDebugThread(debug::Events*); + Result ParseDebugThreadBody(debug::Thread* thread); Result ParseClear(); Result ParseClearColor(); Result ParseExpect(); diff --git a/src/command.h b/src/command.h index dc4e3d6d2..fe825cd26 100644 --- a/src/command.h +++ b/src/command.h @@ -25,6 +25,7 @@ #include "amber/value.h" #include "src/buffer.h" #include "src/command_data.h" +#include "src/debug.h" #include "src/pipeline_data.h" #include "src/sampler.h" @@ -115,11 +116,20 @@ class Command { /// Returns the input file line this command was declared on. size_t GetLine() const { return line_; } + /// Sets the debug script to run for this command. + void SetDebugScript(std::unique_ptr&& debug) { + debug_ = std::move(debug); + } + + /// Returns the optional debug script associated with this command. + debug::Script* GetDebugScript() { return debug_.get(); } + protected: explicit Command(Type type); Type command_type_; size_t line_ = 1; + std::unique_ptr debug_; }; /// Base class for commands which contain a pipeline. From c8239eb452667c4215ac32fcbffb39e538671ee2 Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 29 Jan 2020 16:34:38 +0000 Subject: [PATCH 4/7] Update dependencies and build for debugger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the new third_party dependencies: • github.com/google/cppdap • github.com/nlohmann/json (required by cppdap) Add the `AMBER_ENABLE_VK_DEBUGGING` option to CMake. This builds and links `cppdap`, but is not currrently referenced in code. Rolled SwiftShader forward, and passed required CMake debugger build flags down. --- .gitignore | 6 ++++-- CMakeLists.txt | 3 +++ DEPS | 15 ++++++++++++--- src/CMakeLists.txt | 4 ++++ src/vulkan/CMakeLists.txt | 4 ++++ third_party/CMakeLists.txt | 25 ++++++++++++++++++------- 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index ece4a6b3b..c93a52e04 100644 --- a/.gitignore +++ b/.gitignore @@ -2,18 +2,20 @@ out third_party/clspv third_party/clspv-clang third_party/clspv-llvm +third_party/cppdap third_party/cpplint third_party/dxc third_party/glslang third_party/googletest +third_party/json third_party/lodepng third_party/shaderc -third_party/spirv-tools third_party/spirv-headers +third_party/spirv-tools third_party/swiftshader third_party/vulkan-headers -third_party/vulkan-validationlayers/ third_party/vulkan-loader +third_party/vulkan-validationlayers/ .vs # Vim swap files diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c7514828..a754c34f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,8 @@ option(AMBER_USE_LOCAL_VULKAN "Build with vulkan in third_party" OFF) option(AMBER_USE_CLSPV "Build with Clspv support" OFF) option(AMBER_ENABLE_SWIFTSHADER "Build using SwiftShader" ${AMBER_ENABLE_SWIFTSHADER}) +option(AMBER_ENABLE_VK_DEBUGGING + "Build with cppdap debugging support" ${AMBER_ENABLE_VK_DEBUGGING}) if (${AMBER_USE_CLSPV} OR ${AMBER_ENABLE_SWIFTSHADER}) set(CMAKE_CXX_STANDARD 14) @@ -148,6 +150,7 @@ add_definitions(-DAMBER_ENABLE_SHADERC=$) add_definitions(-DAMBER_ENABLE_DXC=$) add_definitions(-DAMBER_ENABLE_CLSPV=$) add_definitions(-DAMBER_ENABLE_LODEPNG=$) +add_definitions(-DAMBER_ENABLE_VK_DEBUGGING=$) set(CMAKE_DEBUG_POSTFIX "") diff --git a/DEPS b/DEPS index f3bcf72fa..93b34131f 100644 --- a/DEPS +++ b/DEPS @@ -5,23 +5,26 @@ vars = { 'khronos_git': 'https://github.com/KhronosGroup', 'llvm_git': 'https://github.com/llvm', 'lvandeve_git': 'https://github.com/lvandeve', - 'swiftshader_git': 'https://swiftshader.googlesource.com', 'microsoft_git': 'https://github.com/Microsoft', + 'nlohmann_git': 'https://github.com/nlohmann', + 'swiftshader_git': 'https://swiftshader.googlesource.com', 'clspv_llvm_revision': 'ac9b2a6297420a461f7b9db9e2dbd67f5f07f301', 'clspv_revision': '50e3cd23a763372a0ccf5c9bbfc21b6c5e2df57d', + 'cppdap_revision': 'de7dffaf6635ffa3c78553bb6b9e11a50c9b86ad', 'cpplint_revision': '26470f9ccb354ff2f6d098f831271a1833701b28', 'dxc_revision': '32168eac845b1dca4b0b3bd086434ec1503a6ae7', 'glslang_revision': '07a55839eed550d84ef62e0c7f503e0d67692708', 'googletest_revision': '10b1902d893ea8cc43c69541d70868f91af3646b', + 'json_revision': 'ad383f66bc48bac18ddf8785d63ef2c6a32aa770', 'lodepng_revision': '5a0dba103893e6b8084be13945a826663917d00a', 'shaderc_revision': '821d564bc7896fdd5d68484e10aac30ea192568f', 'spirv_headers_revision': 'dc77030acc9c6fe7ca21fff54c5a9d7b532d7da6', 'spirv_tools_revision': '97f1d485b76303ea7094fa164c0cc770b79f6f78', - 'swiftshader_revision': '6c3dc3581eaf4345c0507d5ac7bb013d55351547', + 'swiftshader_revision': '5ba2a5b9a43cc07f1d3042d649907ace92e4cb58', 'vulkan_headers_revision': '7264358702061d3ed819d62d3d6fd66ab1da33c3', - 'vulkan_validationlayers_revision': 'c51d450ef72c36014e821a289f38f8a5b5ea1010', 'vulkan_loader_revision': '44ac9b2f406f863c69a297a77bd23c28fa29e78d', + 'vulkan_validationlayers_revision': 'c51d450ef72c36014e821a289f38f8a5b5ea1010', } deps = { @@ -31,6 +34,9 @@ deps = { 'third_party/clspv-llvm': Var('llvm_git') + '/llvm-project.git@' + Var('clspv_llvm_revision'), + 'third_party/cppdap': Var('google_git') + '/cppdap.git@' + + Var('cppdap_revision'), + 'third_party/cpplint': Var('google_git') + '/styleguide.git@' + Var('cpplint_revision'), @@ -40,6 +46,9 @@ deps = { 'third_party/googletest': Var('google_git') + '/googletest.git@' + Var('googletest_revision'), + 'third_party/json': Var('nlohmann_git') + '/json.git@' + + Var('json_revision'), + 'third_party/glslang': Var('khronos_git') + '/glslang.git@' + Var('glslang_revision'), diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a8a43a26f..57843f32d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -102,6 +102,10 @@ if (${AMBER_ENABLE_SHADERC}) target_link_libraries(libamber shaderc SPIRV) endif() +if (${AMBER_ENABLE_VK_DEBUGGING}) + target_link_libraries(libamber cppdap) +endif() + if (NOT MSVC AND NOT ANDROID) target_link_libraries(libamber pthread) endif() diff --git a/src/vulkan/CMakeLists.txt b/src/vulkan/CMakeLists.txt index 0688fa11d..607ccd861 100644 --- a/src/vulkan/CMakeLists.txt +++ b/src/vulkan/CMakeLists.txt @@ -53,6 +53,10 @@ if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") target_compile_options(libamberenginevulkan PRIVATE -Wno-zero-as-null-pointer-constant) endif() +if (${AMBER_ENABLE_VK_DEBUGGING}) + target_link_libraries(libamberenginevulkan cppdap) +endif() + add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/src/vk-wrappers.inc.fake COMMAND diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index b5bd5d74a..3cf9058d1 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -65,14 +65,25 @@ if (${AMBER_USE_LOCAL_VULKAN}) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vulkan-validationlayers) endif() +if (${AMBER_ENABLE_VK_DEBUGGING}) + set(CPPDAP_JSON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/json) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/cppdap) +endif() + if (${AMBER_ENABLE_SWIFTSHADER}) - set(SWIFTSHADER_BUILD_EGL FALSE CACHE BOOL FALSE) - set(SWIFTSHADER_BUILD_GLESv2 FALSE CACHE BOOL FALSE) - set(SWIFTSHADER_BUILD_GLES_CM FALSE CACHE BOOL FALSE) - set(SWIFTSHADER_BUILD_VULKAN TRUE CACHE BOOL TRUE) - set(SWIFTSHADER_BUILD_SAMPLES FALSE CACHE BOOL FALSE) - set(SWIFTSHADER_BUILD_TESTS FALSE CACHE BOOL FALSE) - set(SWIFTSHADER_WARNINGS_AS_ERRORS FALSE CACHE BOOL FALSE) + set(SWIFTSHADER_BUILD_EGL FALSE) + set(SWIFTSHADER_BUILD_GLESv2 FALSE) + set(SWIFTSHADER_BUILD_GLES_CM FALSE) + set(SWIFTSHADER_BUILD_VULKAN TRUE) + set(SWIFTSHADER_BUILD_SAMPLES FALSE) + set(SWIFTSHADER_BUILD_TESTS FALSE) + set(SWIFTSHADER_WARNINGS_AS_ERRORS FALSE) + + if (${AMBER_ENABLE_VK_DEBUGGING}) + set(SWIFTSHADER_ENABLE_VULKAN_DEBUGGER TRUE) + set(SWIFTSHADER_BUILD_CPPDAP FALSE) # Already built above + endif (${AMBER_ENABLE_VK_DEBUGGING}) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/swiftshader) endif() From dfe2a76899da55857ee3d2160bfe09e65f63f77e Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 29 Jan 2020 17:21:14 +0000 Subject: [PATCH 5/7] Implement debugger engine testing This is an initial, rough version, but implements enough to perform single line stepping, and verifying location and local values. See docs/debugger.md for details. --- Android.mk | 1 + src/dawn/engine_dawn.h | 5 + src/engine.cc | 2 + src/engine.h | 16 + src/executor.cc | 24 + src/executor_test.cc | 5 + src/vulkan/CMakeLists.txt | 1 + src/vulkan/engine_vulkan.h | 7 + src/vulkan/engine_vulkan_debugger.cc | 977 +++++++++++++++++++++++++++ 9 files changed, 1038 insertions(+) create mode 100644 src/vulkan/engine_vulkan_debugger.cc diff --git a/Android.mk b/Android.mk index 2e918d07b..b37324582 100644 --- a/Android.mk +++ b/Android.mk @@ -59,6 +59,7 @@ LOCAL_SRC_FILES:= \ src/vulkan/descriptor.cc \ src/vulkan/device.cc \ src/vulkan/engine_vulkan.cc \ + src/vulkan/engine_vulkan_debugger.cc \ src/vulkan/frame_buffer.cc \ src/vulkan/graphics_pipeline.cc \ src/vulkan/image_descriptor.cc \ diff --git a/src/dawn/engine_dawn.h b/src/dawn/engine_dawn.h index 540a7051a..2756949a6 100644 --- a/src/dawn/engine_dawn.h +++ b/src/dawn/engine_dawn.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "dawn/dawncpp.h" @@ -61,6 +62,10 @@ class EngineDawn : public Engine { const PatchParameterVerticesCommand* cmd) override; Result DoBuffer(const BufferCommand* cmd) override; + std::pair GetDebugger() override { + return {nullptr, Result("Dawn does not currently support a debugger")}; + } + private: // Returns the Dawn-specific render pipeline for the given command, // if it exists. Returns nullptr otherwise. diff --git a/src/engine.cc b/src/engine.cc index 4f65a8a34..7577e432a 100644 --- a/src/engine.cc +++ b/src/engine.cc @@ -51,4 +51,6 @@ Engine::Engine() = default; Engine::~Engine() = default; +Engine::Debugger::~Debugger() = default; + } // namespace amber diff --git a/src/engine.h b/src/engine.h index 7b1f10efe..1b0aab227 100644 --- a/src/engine.h +++ b/src/engine.h @@ -17,6 +17,7 @@ #include #include +#include #include #include "amber/amber.h" @@ -53,6 +54,16 @@ struct EngineData { /// 5. Engine destructor is called. class Engine { public: + /// Debugger is the interface to the engine's shader debugger. + class Debugger : public debug::Events { + public: + ~Debugger(); + + /// Flush waits for all the debugger commands issued to complete, and + /// returns a Result that includes any debugger test failure. + virtual Result Flush() = 0; + }; + /// Creates a new engine of the requested |type|. static std::unique_ptr Create(EngineType type); @@ -106,6 +117,11 @@ class Engine { /// This covers both Vulkan buffers and images. virtual Result DoBuffer(const BufferCommand* cmd) = 0; + /// GetDebugger returns the shader debugger from the engine. + /// If the engine does not support a shader debugger then the Result will be a + /// failure. + virtual std::pair GetDebugger() = 0; + /// Sets the engine data to use. void SetEngineData(const EngineData& data) { engine_data_ = data; } diff --git a/src/executor.cc b/src/executor.cc index 17c2cd29a..be058f158 100644 --- a/src/executor.cc +++ b/src/executor.cc @@ -83,6 +83,8 @@ Result Executor::Execute(Engine* engine, if (options->execution_type == ExecutionType::kPipelineCreateOnly) return {}; + Engine::Debugger* debugger = nullptr; + // Process Commands for (const auto& cmd : script->GetCommands()) { if (options->delegate && options->delegate->LogExecuteCalls()) { @@ -90,9 +92,31 @@ Result Executor::Execute(Engine* engine, cmd->ToString()); } + auto dbg_script = cmd->GetDebugScript(); + if (dbg_script != nullptr) { + if (debugger == nullptr) { + // Lazilly obtain the debugger from the engine. + Result res; + std::tie(debugger, res) = engine->GetDebugger(); + if (!res.IsSuccess()) { + return res; + } + } + // Run the debugger script on the debugger for this command. + // This will run concurrently with the command. + dbg_script->Run(debugger); + } + Result r = ExecuteCommand(engine, cmd.get()); if (!r.IsSuccess()) return r; + + if (debugger != nullptr) { + // Collect the debugger test results. + r = debugger->Flush(); + if (!r.IsSuccess()) + return r; + } } return {}; } diff --git a/src/executor_test.cc b/src/executor_test.cc index 7ba866071..3f8cd2df5 100644 --- a/src/executor_test.cc +++ b/src/executor_test.cc @@ -159,6 +159,11 @@ class EngineStub : public Engine { return {}; } + std::pair GetDebugger() override { + return {nullptr, + Result("EngineStub does not currently support a debugger")}; + } + private: bool fail_clear_command_ = false; bool fail_clear_color_command_ = false; diff --git a/src/vulkan/CMakeLists.txt b/src/vulkan/CMakeLists.txt index 607ccd861..f3a0c8de1 100644 --- a/src/vulkan/CMakeLists.txt +++ b/src/vulkan/CMakeLists.txt @@ -21,6 +21,7 @@ set(VULKAN_ENGINE_SOURCES device.cc descriptor.cc engine_vulkan.cc + engine_vulkan_debugger.cc frame_buffer.cc graphics_pipeline.cc image_descriptor.cc diff --git a/src/vulkan/engine_vulkan.h b/src/vulkan/engine_vulkan.h index c875e075a..7897ca25b 100644 --- a/src/vulkan/engine_vulkan.h +++ b/src/vulkan/engine_vulkan.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "amber/vulkan_header.h" @@ -37,6 +38,8 @@ namespace vulkan { /// Engine implementation based on Vulkan. class EngineVulkan : public Engine { public: + class VkDebugger; + EngineVulkan(); ~EngineVulkan() override; @@ -60,6 +63,8 @@ class EngineVulkan : public Engine { const PatchParameterVerticesCommand* cmd) override; Result DoBuffer(const BufferCommand* cmd) override; + std::pair GetDebugger() override; + private: struct PipelineInfo { std::unique_ptr vk_pipeline; @@ -87,6 +92,8 @@ class EngineVulkan : public Engine { std::unique_ptr pool_; std::map pipeline_map_; + + std::unique_ptr debugger_; }; } // namespace vulkan diff --git a/src/vulkan/engine_vulkan_debugger.cc b/src/vulkan/engine_vulkan_debugger.cc new file mode 100644 index 000000000..08c5ef94c --- /dev/null +++ b/src/vulkan/engine_vulkan_debugger.cc @@ -0,0 +1,977 @@ +// Copyright 2020 The Amber Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "src/vulkan/engine_vulkan.h" + +#if AMBER_ENABLE_VK_DEBUGGING + +#include // NOLINT(build/c++11) +#include // NOLINT(build/c++11) +#include +#include // NOLINT(build/c++11) +#include +#include // NOLINT(build/c++11) +#include + +#include "dap/network.h" +#include "dap/protocol.h" +#include "dap/session.h" + +// Set to 1 to enable verbose debugger logging +#define ENABLE_DEBUGGER_LOG 0 + +#if ENABLE_DEBUGGER_LOG +#define DEBUGGER_LOG(...) \ + do { \ + printf(__VA_ARGS__); \ + printf("\n"); \ + } while (false) +#else +#define DEBUGGER_LOG(...) +#endif + +namespace amber { +namespace vulkan { + +namespace { + +static constexpr auto kThreadTimeout = std::chrono::minutes(1); + +// Event provides a basic wait-and-signal synchronization primitive. +class Event { + public: + // Wait blocks until the event is fired. + void Wait() { + std::unique_lock lock(mutex_); + cv_.wait(lock, [&] { return signalled_; }); + } + + // Wait blocks until the event is fired, or the timeout is reached. + // If the Event was signalled, then Wait returns true, otherwise false. + template + bool Wait(const std::chrono::duration& duration) { + std::unique_lock lock(mutex_); + return cv_.wait_for(lock, duration, [&] { return signalled_; }); + } + + // Signal signals the Event, unblocking any calls to Wait. + void Signal() { + std::unique_lock lock(mutex_); + signalled_ = true; + cv_.notify_all(); + } + + private: + std::condition_variable cv_; + std::mutex mutex_; + bool signalled_ = false; +}; + +// Split slices str into all substrings separated by sep and returns a vector of +// the substrings between those separators. +std::vector Split(const std::string& str, const std::string& sep) { + std::vector out; + std::size_t cur = 0; + std::size_t prev = 0; + while ((cur = str.find(sep, prev)) != std::string::npos) { + out.push_back(str.substr(prev, cur - prev)); + prev = cur + 1; + } + out.push_back(str.substr(prev)); + return out; +} + +// Forward declaration. +struct Variable; + +// Variables is a list of Variable (), with helper methods. +class Variables : public std::vector { + public: + inline const Variable* Find(const std::string& name) const; + inline std::string AllNames() const; +}; + +// Variable holds a debugger returned named value (local, global, etc). +// Variables can hold child variables (for structs, arrays, etc). +struct Variable { + std::string name; + std::string value; + Variables children; + + // Get parses the Variable value for the requested type, assigning the result + // to |out|. Returns true on success, otherwise false. + bool Get(int* out) const { + *out = std::atoi(value.c_str()); + return true; // TODO(bclayton): Verify the value parsed correctly. + } + + bool Get(uint32_t* out) const { + *out = static_cast(std::atoi(value.c_str())); + return true; // TODO(bclayton): Verify the value parsed correctly. + } + + bool Get(int64_t* out) const { + *out = static_cast(std::atoi(value.c_str())); + return true; // TODO(bclayton): Verify the value parsed correctly. + } + + bool Get(float* out) const { + *out = std::atof(value.c_str()); + return true; // TODO(bclayton): Verify the value parsed correctly. + } + + bool Get(double* out) const { + *out = std::atof(value.c_str()); + return true; // TODO(bclayton): Verify the value parsed correctly. + } + + bool Get(std::string* out) const { + *out = value; + return true; + } + + template + bool Get(std::tuple* out) const { + auto x = children.Find("x"); + auto y = children.Find("y"); + auto z = children.Find("z"); + if (x != nullptr && y != nullptr && z != nullptr) { + T elX; + T elY; + T elZ; + if (x->Get(&elX) && y->Get(&elY) && z->Get(&elZ)) { + *out = std::tuple{elX, elY, elZ}; + return true; + } + } + return false; + } +}; + +const Variable* Variables::Find(const std::string& name) const { + for (auto& child : *this) { + if (child.name == name) { + return &child; + } + } + return nullptr; +} + +std::string Variables::AllNames() const { + std::string out; + for (auto& var : *this) { + if (out.size() > 0) { + out += ", "; + } + out += "'" + var.name + "'"; + } + return out; +} + +// Client wraps a dap::Session and a error handler, and provides a more +// convenient interface for talking to the debugger. Client also provides basic +// immutable data caching to help performance. +class Client { + static constexpr const char* kLocals = "locals"; + static constexpr const char* kLane = "Lane"; + + public: + using ErrorHandler = std::function; + using SourceLines = std::vector; + + Client(const std::shared_ptr& session, + const ErrorHandler& onerror) + : session_(session), onerror_(onerror) {} + + // TopStackFrame retrieves the frame at the top of the thread's call stack. + // Returns true on success, false on error. + bool TopStackFrame(dap::integer threadId, dap::StackFrame* frame) { + dap::StackTraceRequest request; + request.threadId = threadId; + auto response = session_->send(request).get(); + if (response.error) { + onerror_(response.error.message); + return false; + } + if (response.response.stackFrames.size() == 0) { + onerror_("Stack frame is empty"); + return false; + } + *frame = response.response.stackFrames.front(); + return true; + } + + // FrameLocation retrieves the current frame source location, and optional + // source line text. + // Returns true on success, false on error. + bool FrameLocation(const dap::StackFrame& frame, + debug::Location* location, + std::string* line = nullptr) { + location->line = frame.line; + + if (!frame.source.has_value()) { + onerror_("Stack frame with name '" + frame.name + "' has no source"); + return false; + } else if (frame.source->path.has_value()) { + location->file = frame.source.value().path.value(); + } else if (frame.source->name.has_value()) { + location->file = frame.source.value().name.value(); + } else { + onerror_("Frame source had no path or name"); + return false; + } + + if (location->line < 1) { + onerror_("Line location is " + std::to_string(location->line)); + return false; + } + + if (line != nullptr) { + SourceLines lines; + if (!SourceContent(frame.source.value(), &lines)) { + return false; + } + if (location->line > lines.size()) { + onerror_("Line " + std::to_string(location->line) + + " is greater than the number of lines in the source file (" + + std::to_string(lines.size()) + ")"); + } + *line = lines[location->line - 1]; + } + + return true; + } + + // SourceContext retrieves the the SourceLines for the given source. + // Returns true on success, false on error. + bool SourceContent(const dap::Source& source, SourceLines* out) { + auto path = source.path.value(""); + if (path != "") { + auto it = sourceCache_.by_path.find(path); + if (it != sourceCache_.by_path.end()) { + *out = it->second; + return true; + } + + // TODO(bclayton) - We shouldn't be doing direct file IO here. We should + // bubble the IO request to the amber 'embedder'. + // See: https://github.com/google/amber/issues/777 + std::ifstream file(path); + if (!file) { + onerror_("Could not open source file '" + path + '"'); + return false; + } + + SourceLines lines; + std::string line; + while (std::getline(file, line)) { + lines.emplace_back(line); + } + + sourceCache_.by_path.emplace(path, lines); + *out = lines; + return true; + } + + if (source.sourceReference.has_value()) { + auto ref = source.sourceReference.value(); + auto it = sourceCache_.by_ref.find(ref); + if (it != sourceCache_.by_ref.end()) { + *out = it->second; + return true; + } + + dap::SourceRequest request; + dap::SourceResponse response; + request.sourceReference = ref; + if (!Send(request, &response)) { + return false; + } + auto lines = Split(response.content, "\n"); + sourceCache_.by_ref.emplace(ref, lines); + *out = lines; + return true; + } + + onerror_("Could not get source content"); + return false; + } + + // Send sends the request to the debugger, waits for the request to complete, + // and then assigns the response to |res|. + // Returns true on success, false on error. + template + bool Send(const REQUEST& request, RESPONSE* res) { + auto r = session_->send(request).get(); + if (r.error) { + onerror_(r.error.message); + return false; + } + *res = r.response; + return true; + } + + // Send sends the request to the debugger, and waits for the request to + // complete. + // Returns true on success, false on error. + template + bool Send(const REQUEST& request) { + using RESPONSE = typename REQUEST::Response; + RESPONSE response; + return Send(request, &response); + } + + // GetVariables fetches the fully traversed set of Variables from the debugger + // for the given reference identifier. + // Returns true on success, false on error. + bool GetVariables(dap::integer variablesRef, Variables* out) { + dap::VariablesRequest request; + dap::VariablesResponse response; + request.variablesReference = variablesRef; + if (!Send(request, &response)) { + return false; + } + for (auto var : response.variables) { + Variable v; + v.name = var.name; + v.value = var.value; + if (var.variablesReference > 0) { + if (!GetVariables(var.variablesReference, &v.children)) { + return false; + } + } + out->emplace_back(v); + } + return true; + } + + // GetLocals fetches the fully traversed set of local Variables from the + // debugger for the given stack frame. + // Returns true on success, false on error. + bool GetLocals(const dap::StackFrame& frame, Variables* out) { + dap::ScopesRequest scopeReq; + dap::ScopesResponse scopeRes; + scopeReq.frameId = frame.id; + if (!Send(scopeReq, &scopeRes)) { + return false; + } + + for (auto scope : scopeRes.scopes) { + if (scope.presentationHint.value("") == kLocals) { + return GetVariables(scope.variablesReference, out); + } + } + + onerror_("Locals scope not found"); + return false; + } + + // GetLane returns a pointer to the Variables representing the thread's SIMD + // lane with the given index, or nullptr if the lane was not found. + const Variables* GetLane(const Variables& lanes, int lane) { + auto out = lanes.Find(std::string(kLane) + " " + std::to_string(lane)); + if (out == nullptr) { + return nullptr; + } + return &out->children; + } + + private: + struct SourceCache { + std::unordered_map by_ref; + std::unordered_map by_path; + }; + + std::shared_ptr session_; + ErrorHandler onerror_; + SourceCache sourceCache_; +}; + +// GlobalInvocationId holds a three-element unsigned integer index, used to +// identifiy a single compute invocation. +struct GlobalInvocationId { + size_t hash() const { return x << 20 | y << 10 | z; } + bool operator==(const GlobalInvocationId& other) const { + return x == other.x && y == other.y && z == other.z; + } + + uint32_t x; + uint32_t y; + uint32_t z; +}; + +// InvocationKey is a tagged-union structure that identifies a single shader +// invocation. +struct InvocationKey { + // Hash is a custom hasher that can enable InvocationKeys to be used as keys + // in std containers. + struct Hash { + size_t operator()(const InvocationKey& key) const; + }; + + enum class Type { kGlobalInvocationId, kVertexIndex }; + union Data { + GlobalInvocationId globalInvocationId; + uint32_t vertexId; + }; + + explicit InvocationKey(const GlobalInvocationId& id); + InvocationKey(Type, const Data&); + + bool operator==(const InvocationKey& other) const; + + // String returns a human-readable description of the key. + std::string String() const; + + Type type; + Data data; +}; + +size_t InvocationKey::Hash::operator()(const InvocationKey& key) const { + size_t hash = 31 * static_cast(key.type); + switch (key.type) { + case Type::kGlobalInvocationId: + hash += key.data.globalInvocationId.hash(); + break; + case Type::kVertexIndex: + hash += key.data.vertexId; + break; + } + return hash; +} + +InvocationKey::InvocationKey(const GlobalInvocationId& id) + : type(Type::kGlobalInvocationId) { + data.globalInvocationId = id; +} + +InvocationKey::InvocationKey(Type type, const Data& data) + : type(type), data(data) {} + +std::string InvocationKey::String() const { + std::stringstream ss; + switch (type) { + case Type::kGlobalInvocationId: { + auto& id = data.globalInvocationId; + ss << "GlobalInvocation(" << id.x << ", " << id.y << ", " << id.z << ")"; + break; + } + case Type::kVertexIndex: + ss << "VertexIndex(" << data.vertexId << ")"; + break; + } + return ss.str(); +} + +bool InvocationKey::operator==(const InvocationKey& other) const { + if (type != other.type) { + return false; + } + switch (type) { + case Type::kGlobalInvocationId: + return data.globalInvocationId == other.data.globalInvocationId; + case Type::kVertexIndex: + return data.vertexId == other.data.vertexId; + } + return false; +} + +// Thread controls and verifies a single debugger thread of execution. +class Thread : public debug::Thread { + using Result = Result; + + public: + Thread(std::shared_ptr session, + int threadId, + int lane, + const debug::Events::OnThread& callback) + : threadId_(threadId), + lane_(lane), + client_(session, [this](const std::string& err) { OnError(err); }) { + // The thread script runs concurrently with other debugger thread scripts. + // Run on a separate amber thread. + thread_ = std::thread([this, callback] { + callback(this); // Begin running the thread script. + done_.Signal(); // Signal when done. + }); + } + + ~Thread() { Flush(); } + + // Flush waits for the debugger thread script to complete, and returns any + // errors encountered. + Result Flush() { + if (done_.Wait(kThreadTimeout)) { + if (thread_.joinable()) { + thread_.join(); + } + } else { + error_ += "Timed out performing actions"; + } + return error_; + } + + // debug::Thread compliance + void StepOver() override { + DEBUGGER_LOG("StepOver()"); + if (error_.IsSuccess()) { + dap::NextRequest request; + request.threadId = threadId_; + client_.Send(request); + } + } + + void StepIn() override { + DEBUGGER_LOG("StepIn()"); + if (error_.IsSuccess()) { + dap::StepInRequest request; + request.threadId = threadId_; + client_.Send(request); + } + } + + void StepOut() override { + DEBUGGER_LOG("StepOut()"); + if (error_.IsSuccess()) { + dap::StepOutRequest request; + request.threadId = threadId_; + client_.Send(request); + } + } + + void Continue() override { + DEBUGGER_LOG("Continue()"); + if (error_.IsSuccess()) { + dap::ContinueRequest request; + request.threadId = threadId_; + client_.Send(request); + } + } + + void ExpectLocation(const debug::Location& location, + const std::string& line) override { + DEBUGGER_LOG("ExpectLocation('%s', %d)", location.file.c_str(), + location.line); + + dap::StackFrame frame; + if (!client_.TopStackFrame(threadId_, &frame)) { + return; + } + + debug::Location got_location; + std::string got_source_line; + if (!client_.FrameLocation(frame, &got_location, &got_source_line)) { + return; + } + + if (got_location.file != location.file) { + OnError("Expected file to be '" + location.file + "' but file was " + + got_location.file); + } else if (got_location.line != location.line) { + OnError("Expected line number to be " + std::to_string(location.line) + + " but line number was " + std::to_string(got_location.line)); + } else if (line != "" && got_source_line != line) { + OnError("Expected source line to be:\n " + line + "\nbut line was:\n " + + got_source_line); + } + } + + void ExpectLocal(const std::string& name, int64_t value) override { + DEBUGGER_LOG("ExpectLocal('%s', %d)", name.c_str(), (int)value); + ExpectLocalT(name, value); + } + + void ExpectLocal(const std::string& name, double value) override { + DEBUGGER_LOG("ExpectLocal('%s', %f)", name.c_str(), value); + ExpectLocalT(name, value); + } + + void ExpectLocal(const std::string& name, const std::string& value) override { + DEBUGGER_LOG("ExpectLocal('%s', '%s')", name.c_str(), value.c_str()); + ExpectLocalT(name, value); + } + + template + void ExpectLocalT(const std::string& name, const T& expect) { + dap::StackFrame frame; + if (!client_.TopStackFrame(threadId_, &frame)) { + return; + } + + Variables locals; + if (!client_.GetLocals(frame, &locals)) { + return; + } + + if (auto lane = client_.GetLane(locals, lane_)) { + auto owner = lane; + const Variable* var = nullptr; + std::string path; + for (auto part : Split(name, ".")) { + var = owner->Find(part); + if (!var) { + if (owner == lane) { + OnError("Local '" + name + "' not found\nAll Locals: " + + lane->AllNames() + ".\nLanes: " + locals.AllNames() + "."); + } else { + OnError("Local '" + path + "' does not contain '" + part + + "'\nChildren: " + owner->AllNames()); + } + return; + } + owner = &var->children; + path += (path.size() > 0) ? "." + part : part; + } + + T got = {}; + if (!var->Get(&got)) { + OnError("Local '" + name + "' was not of expected type"); + return; + } + + if (got != expect) { + std::stringstream ss; + ss << "Local '" << name << "' did not have expected value. Value is '" + << got << "', expected '" << expect << "'"; + OnError(ss.str()); + return; + } + } + } + + private: + void OnError(const std::string& err) { + DEBUGGER_LOG("ERROR: %s", err.c_str()); + error_ += err; + } + + const dap::integer threadId_; + const int lane_; + Client client_; + std::thread thread_; + Event done_; + Result error_; +}; + +} // namespace + +// EngineVulkan::VkDebugger is a private implementation of the Engine::Debugger +// interface. +class EngineVulkan::VkDebugger : public Engine::Debugger { + static constexpr const char* kComputeShaderFunctionName = "ComputeShader"; + static constexpr const char* kVertexShaderFunctionName = "VertexShader"; + static constexpr const char* kGlobalInvocationId = "globalInvocationId"; + static constexpr const char* kVertexIndex = "vertexIndex"; + + public: + /// Connect establishes the connection to the shader debugger. Must be + /// called before any of the |debug::Events| methods. + Result Connect() { + constexpr int kMaxAttempts = 10; + // The socket might take a while to open - retry connecting. + for (int attempt = 0; attempt < kMaxAttempts; attempt++) { + auto connection = dap::net::connect("localhost", 19020); + if (!connection) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + continue; + } + + // Socket opened. Create the debugger session and bind. + session_ = dap::Session::create(); + session_->bind(connection); + + // Register the thread stopped event. + // This is fired when breakpoints are hit (amongst other reasons). + // See: + // https://microsoft.github.io/debug-adapter-protocol/specification#Events_Stopped + session_->registerHandler([&](const dap::StoppedEvent& event) { + DEBUGGER_LOG("THREAD STOPPED. Reason: %s", event.reason.c_str()); + if (event.reason == "function breakpoint") { + OnBreakpointHit(event.threadId.value(0)); + } + }); + + // Start the debugger initialization sequence. + // See: https://microsoft.github.io/debug-adapter-protocol/overview for + // details. + + dap::InitializeRequest init_req = {}; + auto init_res = session_->send(init_req).get(); + if (init_res.error) { + DEBUGGER_LOG("InitializeRequest failed: %s", + init_res.error.message.c_str()); + return Result(init_res.error.message); + } + + // Set breakpoints on the various shader types, we do this even if we + // don't actually care about these threads. Once the breakpoint is hit, + // the pendingThreads_ map is probed, if nothing matches the thread is + // resumed. + // TODO(bclayton): Once we have conditional breakpoint support, we can + // reduce the number of breakpoints / scope of breakpoints. + dap::SetFunctionBreakpointsRequest fbp_req = {}; + dap::FunctionBreakpoint fbp = {}; + fbp.name = kComputeShaderFunctionName; + fbp_req.breakpoints.emplace_back(fbp); + fbp.name = kVertexShaderFunctionName; + fbp_req.breakpoints.emplace_back(fbp); + auto fbp_res = session_->send(fbp_req).get(); + if (fbp_res.error) { + DEBUGGER_LOG("SetFunctionBreakpointsRequest failed: %s", + fbp_res.error.message.c_str()); + return Result(fbp_res.error.message); + } + + // ConfigurationDone signals the initialization has completed. + dap::ConfigurationDoneRequest cfg_req = {}; + auto cfg_res = session_->send(cfg_req).get(); + if (cfg_res.error) { + DEBUGGER_LOG("ConfigurationDoneRequest failed: %s", + cfg_res.error.message.c_str()); + return Result(cfg_res.error.message); + } + + return Result(); + } + return Result("Unable to connect to debugger"); + } + + // Flush checks that all breakpoints were hit, waits for all threads to + // complete, and returns the globbed together results for all threads. + Result Flush() override { + Result result; + { + std::unique_lock lock(error_mutex_); + result += error_; + } + { + std::unique_lock lock(threads_mutex_); + for (auto& pending : pendingThreads_) { + result += "Thread did not run: " + pending.first.String(); + } + for (auto& thread : runningThreads_) { + result += thread->Flush(); + } + runningThreads_.clear(); + } + return result; + } + + // debug::Events compliance + void BreakOnComputeGlobalInvocation( + uint32_t x, + uint32_t y, + uint32_t z, + const debug::Events::OnThread& callback) override { + std::unique_lock lock(threads_mutex_); + pendingThreads_.emplace(GlobalInvocationId{x, y, z}, callback); + }; + + void BreakOnVertexIndex(uint32_t index, const OnThread& callback) override { + InvocationKey::Data data; + data.vertexId = index; + auto key = InvocationKey{InvocationKey::Type::kVertexIndex, data}; + std::unique_lock lock(threads_mutex_); + pendingThreads_.emplace(key, callback); + } + + private: + // OnBreakpointHit is called when a debugger breakpoint is hit (breakpoints + // are set at shader entry points). pendingThreads_ is checked to see if this + // thread needs testing, and if so, creates a new ::Thread. + // If there's no pendingThread_ entry for the given thread, it is resumed to + // allow the shader to continue executing. + void OnBreakpointHit(dap::integer threadId) { + DEBUGGER_LOG("Breakpoint hit: thread %d", (int)threadId); + Client client(session_, [this](const std::string& err) { OnError(err); }); + + std::unique_lock lock(threads_mutex_); + for (auto it = pendingThreads_.begin(); it != pendingThreads_.end(); it++) { + auto& key = it->first; + auto& callback = it->second; + switch (key.type) { + case InvocationKey::Type::kGlobalInvocationId: { + auto invocation_id = key.data.globalInvocationId; + int lane; + if (FindGlobalInvocationId(threadId, invocation_id, &lane)) { + DEBUGGER_LOG("Breakpoint hit: GetGlobalInvocationId: [%d, %d, %d]", + invocation_id.x, invocation_id.y, invocation_id.z); + auto thread = + MakeUnique(session_, threadId, lane, callback); + runningThreads_.emplace_back(std::move(thread)); + pendingThreads_.erase(it); + return; + } + break; + } + case InvocationKey::Type::kVertexIndex: { + auto vertex_id = key.data.vertexId; + int lane; + if (FindVertexIndex(threadId, vertex_id, &lane)) { + DEBUGGER_LOG("Breakpoint hit: VertexId: %d", vertex_id); + auto thread = + MakeUnique(session_, threadId, lane, callback); + runningThreads_.emplace_back(std::move(thread)); + pendingThreads_.erase(it); + return; + } + break; + } + } + } + + // No pending tests for this thread. Let it carry on... + dap::ContinueRequest request; + request.threadId = threadId; + client.Send(request); + } + + // FindGlobalInvocationId looks for the compute shader's global invocation id + // in the stack frames' locals, returning true if found, and assigns the index + // of the SIMD lane it was found in to |lane|. + // TODO(bclayton): This value should probably be in the globals, not locals! + bool FindGlobalInvocationId(dap::integer threadId, + const GlobalInvocationId& id, + int* lane) { + Client client(session_, [this](const std::string& err) { OnError(err); }); + + dap::StackFrame frame; + if (!client.TopStackFrame(threadId, &frame)) { + return false; + } + + dap::ScopesRequest scopeReq; + dap::ScopesResponse scopeRes; + scopeReq.frameId = frame.id; + if (!client.Send(scopeReq, &scopeRes)) { + return false; + } + + Variables locals; + if (!client.GetLocals(frame, &locals)) { + return false; + } + + for (int i = 0;; i++) { + auto lane_var = client.GetLane(locals, i); + if (!lane_var) { + break; + } + if (auto var = lane_var->Find(kGlobalInvocationId)) { + std::tuple got; + if (var->Get(&got)) { + uint32_t x; + uint32_t y; + uint32_t z; + std::tie(x, y, z) = got; + if (x == id.x && y == id.y && z == id.z) { + *lane = i; + return true; + } + } + } + } + + return false; + } + + // FindVertexIndex looks for the requested vertex shader's vertex index in the + // stack frames' locals, returning true if found, and assigns the index of the + // SIMD lane it was found in to |lane|. + // TODO(bclayton): This value should probably be in the globals, not locals! + bool FindVertexIndex(dap::integer threadId, uint32_t index, int* lane) { + Client client(session_, [this](const std::string& err) { OnError(err); }); + + dap::StackFrame frame; + if (!client.TopStackFrame(threadId, &frame)) { + return false; + } + + dap::ScopesRequest scopeReq; + dap::ScopesResponse scopeRes; + scopeReq.frameId = frame.id; + if (!client.Send(scopeReq, &scopeRes)) { + return false; + } + + Variables locals; + if (!client.GetLocals(frame, &locals)) { + return false; + } + + for (int i = 0;; i++) { + auto lane_var = client.GetLane(locals, i); + if (!lane_var) { + break; + } + if (auto var = lane_var->Find(kVertexIndex)) { + uint32_t got; + if (var->Get(&got)) { + if (got == index) { + *lane = i; + return true; + } + } + } + } + + return false; + } + + void OnError(const std::string& error) { + DEBUGGER_LOG("ERROR: %s", error.c_str()); + error_ += error; + } + + using PendingThreadsMap = std::unordered_map; + using ThreadVector = std::vector>; + std::shared_ptr session_; + std::mutex threads_mutex_; + PendingThreadsMap pendingThreads_; // guarded by threads_mutex_ + ThreadVector runningThreads_; // guarded by threads_mutex_ + std::mutex error_mutex_; + Result error_; // guarded by error_mutex_ +}; + +std::pair EngineVulkan::GetDebugger() { + if (!debugger_) { + auto debugger = new VkDebugger(); + debugger_.reset(debugger); + auto res = debugger->Connect(); + if (!res.IsSuccess()) { + return {nullptr, res}; + } + } + return {debugger_.get(), Result()}; +} + +} // namespace vulkan +} // namespace amber + +#else // AMBER_ENABLE_VK_DEBUGGING + +namespace amber { +namespace vulkan { + +std::pair EngineVulkan::GetDebugger() { + return {nullptr, + Result("Amber was not built with AMBER_ENABLE_VK_DEBUGGING enabled")}; +} + +} // namespace vulkan +} // namespace amber + +#endif // AMBER_ENABLE_VK_DEBUGGING From d0a89d1a4c440536074a283f897c480f58054cde Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Wed, 29 Jan 2020 17:23:02 +0000 Subject: [PATCH 6/7] Debugger: Add basic debugger test scripts. One for SPIR-V debugging, one for HLSL debugging using `OpenCL.DebugInfo.100`. --- tests/cases/debugger_hlsl_line_stepping.amber | 354 ++++++++++++++++++ .../cases/debugger_spirv_line_stepping.amber | 173 +++++++++ tests/run_tests.py | 9 + 3 files changed, 536 insertions(+) create mode 100644 tests/cases/debugger_hlsl_line_stepping.amber create mode 100644 tests/cases/debugger_spirv_line_stepping.amber diff --git a/tests/cases/debugger_hlsl_line_stepping.amber b/tests/cases/debugger_hlsl_line_stepping.amber new file mode 100644 index 000000000..c7e04c902 --- /dev/null +++ b/tests/cases/debugger_hlsl_line_stepping.amber @@ -0,0 +1,354 @@ +#!amber +# Copyright 2020 The Amber Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SET ENGINE_DATA fence_timeout_ms 1000000 + +SHADER vertex vtex_shader SPIRV-ASM +; SPIR-V +; Version: 1.0 +; Generator: Khronos SPIR-V Tools Assembler; 0 +; Bound: 28 +; Schema: 0 + OpCapability Shader + %DbgExt = OpExtInstImport "OpenCL.DebugInfo.100" + OpMemoryModel Logical GLSL450 + OpEntryPoint Vertex %main "main" %pos %color %gl_Position %out_var_COLOR + %src = OpString "simple_vs.hlsl" + %code = OpString " +struct VS_OUTPUT { + float4 pos : SV_POSITION; + float4 color : COLOR; +}; + +VS_OUTPUT main(float4 pos : POSITION, + float4 color : COLOR) { + VS_OUTPUT vout; + vout.pos = pos; + vout.color = color; + return vout; +} +" + OpSource HLSL 600 %src " +struct VS_OUTPUT { + float4 pos : SV_POSITION; + float4 color : COLOR; +}; + +VS_OUTPUT main(float4 pos : POSITION, + float4 color : COLOR) { + VS_OUTPUT vout; + vout.pos = pos; + vout.color = color; + return vout; +} +" + OpName %out_var_COLOR "out.var.COLOR" + OpName %main "main" + OpName %VS_OUTPUT "VS_OUTPUT" + OpMemberName %VS_OUTPUT 0 "pos" + OpMemberName %VS_OUTPUT 1 "color" + OpName %pos "pos" + OpName %color "color" + OpName %vout "vout" + OpDecorate %gl_Position BuiltIn Position + OpDecorate %pos Location 0 + OpDecorate %color Location 1 + OpDecorate %out_var_COLOR Location 0 + %int = OpTypeInt 32 1 + %int_0 = OpConstant %int 0 + %int_1 = OpConstant %int 1 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%_ptr_Output_v4float = OpTypePointer Output %v4float + %void = OpTypeVoid + %16 = OpTypeFunction %void +%_ptr_Function_v4float = OpTypePointer Function %v4float + %VS_OUTPUT = OpTypeStruct %v4float %v4float + %18 = OpTypeFunction %VS_OUTPUT %_ptr_Function_v4float %_ptr_Function_v4float +%_ptr_Function_VS_OUTPUT = OpTypePointer Function %VS_OUTPUT + +; Compilation Unit +%dbg_src = OpExtInst %void %DbgExt DebugSource %src %code +%comp_unit = OpExtInst %void %DbgExt DebugCompilationUnit 2 4 %dbg_src HLSL + +; Type names +%VS_OUTPUT_name = OpString "struct VS_OUTPUT" +%float_name = OpString "float" +%VS_OUTPUT_pos_name = OpString "pos" +%VS_OUTPUT_color_name = OpString "color" +%VS_OUTPUT_linkage_name = OpString "VS_OUTPUT" +%main_name = OpString "main" +%main_linkage_name = OpString "VS_OUTPUT_main_v4f_v4f" +%pos_name = OpString "pos" +%color_name = OpString "color" +%vout_name = OpString "vout" + +; Type sizes in bit unit. For example, 128 means "128 bits" +%int_32 = OpConstant %int 32 +%int_128 = OpConstant %int 128 + +; Type information +; %VS_OUTPUT_info and %VS_OUTPUT_pos_info have cycling reference +%VS_OUTPUT_info = OpExtInst %void %DbgExt DebugTypeComposite %VS_OUTPUT_name Structure %dbg_src 1 1 %comp_unit %VS_OUTPUT_linkage_name %int_128 FlagIsPublic %VS_OUTPUT_pos_info %VS_OUTPUT_color_info +%float_info = OpExtInst %void %DbgExt DebugTypeBasic %float_name %int_32 Float +%v4float_info = OpExtInst %void %DbgExt DebugTypeVector %float_info 4 +%VS_OUTPUT_pos_info = OpExtInst %void %DbgExt DebugTypeMember %VS_OUTPUT_pos_name %v4float_info %dbg_src 2 3 %VS_OUTPUT_info %int_0 %int_128 FlagIsPublic +%VS_OUTPUT_color_info = OpExtInst %void %DbgExt DebugTypeMember %VS_OUTPUT_color_name %v4float_info %dbg_src 3 3 %VS_OUTPUT_info %int_128 %int_128 FlagIsPublic +%main_type = OpExtInst %void %DbgExt DebugTypeFunction FlagIsPublic %VS_OUTPUT_info %v4float_info %v4float_info + +; DebugExpression without any information +%null_expr = OpExtInst %void %DbgExt DebugExpression + +; Lexical scope for main +%main_lex_scope = OpExtInst %void %DbgExt DebugLexicalBlock %dbg_src 6 1 %comp_unit + +; Function information +%main_func = OpExtInst %void %DbgExt DebugFunction %main_name %main_type %dbg_src 6 1 %main_lex_scope %main_linkage_name FlagIsPublic 7 %src_main + +; Local variable information +%pos_info = OpExtInst %void %DbgExt DebugLocalVariable %pos_name %v4float_info %dbg_src 6 16 %main_lex_scope FlagIsLocal 0 +%color_info = OpExtInst %void %DbgExt DebugLocalVariable %color_name %v4float_info %dbg_src 7 16 %main_lex_scope FlagIsLocal 1 +%vout_info = OpExtInst %void %DbgExt DebugLocalVariable %vout_name %VS_OUTPUT_info %dbg_src 8 3 %main_lex_scope FlagIsLocal + + %pos = OpVariable %_ptr_Input_v4float Input + +; Declaration of "float4 pos : POSITION" argument of main() +%pos_decl = OpExtInst %void %DbgExt DebugDeclare %pos_info %pos %null_expr + + %color = OpVariable %_ptr_Input_v4float Input + +; Declaration of "float4 color : COLOR" argument of main() +%color_decl = OpExtInst %void %DbgExt DebugDeclare %color_info %color %null_expr + +%gl_Position = OpVariable %_ptr_Output_v4float Output +%out_var_COLOR = OpVariable %_ptr_Output_v4float Output + %main = OpFunction %void None %16 + %20 = OpLabel + +; Start the scope of function "main()" +%main_scope = OpExtInst %void %DbgExt DebugScope %main_lex_scope + + %vout = OpVariable %_ptr_Function_VS_OUTPUT Function + +; Declaration of "VS_OUTPUT vout" local variable in main() +%vout_decl = OpExtInst %void %DbgExt DebugDeclare %vout_info %vout %null_expr + + OpLine %src 9 3 + %21 = OpLoad %v4float %pos + %22 = OpAccessChain %_ptr_Function_v4float %vout %int_0 + +; Tracking value of "float4 pos : SV_POSITION" member of "vout" +%vout_pos_value = OpExtInst %void %DbgExt DebugValue %vout_info %22 %null_expr %int_0 + + OpStore %22 %21 + OpLine %src 10 3 + %23 = OpLoad %v4float %color + %24 = OpAccessChain %_ptr_Function_v4float %vout %int_1 + +; Tracking value of "float4 color : COLOR" member of "vout" +%vout_color_value = OpExtInst %void %DbgExt DebugValue %vout_info %24 %null_expr %int_1 + + OpStore %24 %23 + OpLine %src 11 3 + %25 = OpLoad %VS_OUTPUT %vout + %26 = OpCompositeExtract %v4float %25 0 + OpStore %gl_Position %26 + %27 = OpCompositeExtract %v4float %25 1 + OpStore %out_var_COLOR %27 + OpLine %src 12 3 + OpReturn + +; End the scope of function "main()" +%main_scope_end = OpExtInst %void %DbgExt DebugNoScope + + OpFunctionEnd +END + +SHADER fragment frag_shader SPIRV-ASM +; SPIR-V +; Version: 1.0 +; Generator: Khronos SPIR-V Tools Assembler; 0 +; Bound: 14 +; Schema: 0 + OpCapability Shader + %DbgExt = OpExtInstImport "OpenCL.DebugInfo.100" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %main "main" %color %out_var_SV_TARGET + OpExecutionMode %main OriginUpperLeft + %src = OpString "simple_ps.hlsl" + %code = OpString "#line 1 \"simple_ps.hlsl\" +float4 main(float4 color : COLOR) : SV_TARGET { + return color; +} +" + OpSource HLSL 600 %src "#line 1 \"simple_ps.hlsl\" +float4 main(float4 color : COLOR) : SV_TARGET { + return color; +} +" + OpName %out_var_SV_TARGET "out.var.SV_TARGET" + OpName %main "main" + OpName %color "color" + OpDecorate %color Location 0 + OpDecorate %out_var_SV_TARGET Location 0 + %int = OpTypeInt 32 1 + %float = OpTypeFloat 32 + %v4float = OpTypeVector %float 4 +%_ptr_Input_v4float = OpTypePointer Input %v4float +%_ptr_Output_v4float = OpTypePointer Output %v4float + %void = OpTypeVoid + %9 = OpTypeFunction %void +%_ptr_Function_v4float = OpTypePointer Function %v4float + %11 = OpTypeFunction %v4float %_ptr_Function_v4float + +; Compilation Unit +%dbg_src = OpExtInst %void %DbgExt DebugSource %src %code +%comp_unit = OpExtInst %void %DbgExt DebugCompilationUnit 2 4 %dbg_src HLSL + +; Type names +%float_name = OpString "float" +%main_name = OpString "main" +%main_linkage_name = OpString "v4f_main_v4f" +%color_name = OpString "color : COLOR" + +; Type sizes in bit unit. For example, 128 means "128 bits" +%int_32 = OpConstant %int 32 +%int_128 = OpConstant %int 128 + +; Type information +%float_info = OpExtInst %void %DbgExt DebugTypeBasic %float_name %int_32 Float +%v4float_info = OpExtInst %void %DbgExt DebugTypeVector %float_info 4 +%main_type = OpExtInst %void %DbgExt DebugTypeFunction FlagIsPublic %v4float_info %v4float_info + +; DebugExpression without any information +%null_expr = OpExtInst %void %DbgExt DebugExpression + +; Lexical scope for main +%main_lex_scope = OpExtInst %void %DbgExt DebugLexicalBlock %dbg_src 1 1 %comp_unit + +; Function information +%main_func = OpExtInst %void %DbgExt DebugFunction %main_name %main_type %dbg_src 1 1 %main_lex_scope %main_linkage_name FlagIsPublic 1 %src_main + +; Local variable information +%color_info = OpExtInst %void %DbgExt DebugLocalVariable %color_name %v4float_info %dbg_src 1 13 %main_lex_scope FlagIsLocal 0 + + OpLine %src 1 28 + %color = OpVariable %_ptr_Input_v4float Input + +; Declaration of "float4 color : COLOR" argument of main() +%color_decl = OpExtInst %void %DbgExt DebugDeclare %color_info %color %null_expr + + OpLine %src 1 37 +%out_var_SV_TARGET = OpVariable %_ptr_Output_v4float Output + OpLine %src 1 1 + %main = OpFunction %void None %9 + %12 = OpLabel + +; Start the scope of function "main()" +%main_scope = OpExtInst %void %DbgExt DebugScope %main_lex_scope + + OpLine %src 2 10 + %13 = OpLoad %v4float %color + OpLine %src 2 3 + OpStore %out_var_SV_TARGET %13 + OpReturn + +; End the scope of function "main()" +%main_scope_end = OpExtInst %void %DbgExt DebugNoScope + + OpFunctionEnd +END + +BUFFER position_buf DATA_TYPE R8G8_SNORM DATA +# Full frame +-128 -128 + 127 127 +-128 127 +-128 -128 + 127 127 + 127 -128 +END + +BUFFER vert_color DATA_TYPE R8G8B8A8_UNORM DATA +255 0 0 255 +255 0 0 255 +255 0 0 255 +255 0 0 255 +255 0 0 255 +255 0 0 255 + + 0 255 0 255 + 0 255 0 255 + 0 255 0 255 + 0 255 0 255 + 0 255 0 255 + 0 255 0 255 + + 0 0 255 255 + 0 0 255 255 + 0 0 255 255 + 0 0 255 255 + 0 0 255 255 + 0 0 255 255 + +127 127 127 255 +127 127 127 255 +127 127 127 255 +127 127 127 255 +127 127 127 255 +127 127 127 255 +END + +BUFFER framebuffer FORMAT B8G8R8A8_UNORM + +PIPELINE graphics pipeline + ATTACH vtex_shader + ATTACH frag_shader + + VERTEX_DATA position_buf LOCATION 0 + VERTEX_DATA vert_color LOCATION 1 + + BIND BUFFER framebuffer AS color LOCATION 0 +END + +CLEAR pipeline + +DEBUG pipeline DRAW_ARRAY AS TRIANGLE_LIST START_IDX 0 COUNT 6 + THREAD VERTEX_INDEX 2 + EXPECT LOCATION "simple_vs.hlsl" 9 " VS_OUTPUT vout;" + EXPECT LOCAL "pos.x" EQ -1.007874 + EXPECT LOCAL "pos.y" EQ 1.000000 + EXPECT LOCAL "pos.z" EQ 0.000000 + EXPECT LOCAL "color.x" EQ 1.000000 + EXPECT LOCAL "color.y" EQ 0.000000 + EXPECT LOCAL "color.z" EQ 0.000000 + STEP_IN + EXPECT LOCATION "simple_vs.hlsl" 10 " vout.pos = pos;" + STEP_IN + EXPECT LOCAL "vout.pos.x" EQ -1.007874 + EXPECT LOCAL "vout.pos.y" EQ 1.000000 + EXPECT LOCAL "vout.pos.z" EQ 0.000000 + EXPECT LOCATION "simple_vs.hlsl" 11 " vout.color = color;" + STEP_IN + EXPECT LOCAL "vout.color.x" EQ 1.000000 + EXPECT LOCAL "vout.color.y" EQ 0.000000 + EXPECT LOCAL "vout.color.z" EQ 0.000000 + EXPECT LOCATION "simple_vs.hlsl" 12 " return vout;" + CONTINUE + END +END + +EXPECT framebuffer IDX 0 0 SIZE 250 250 EQ_RGB 255 0 0 diff --git a/tests/cases/debugger_spirv_line_stepping.amber b/tests/cases/debugger_spirv_line_stepping.amber new file mode 100644 index 000000000..e65773f7c --- /dev/null +++ b/tests/cases/debugger_spirv_line_stepping.amber @@ -0,0 +1,173 @@ +#!amber +# Copyright 2020 The Amber Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +SET ENGINE_DATA fence_timeout_ms 1000000 + +# #version 450 +# layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; +# layout(binding = 0, std430) buffer InBuffer +# { +# int Data[]; +# } In; +# layout(binding = 1, std430) buffer OutBuffer +# { +# int Data[]; +# } Out; +# void main() +# { +# Out.Data[gl_GlobalInvocationID.x] = In.Data[gl_GlobalInvocationID.x]; +# } +SHADER compute mah_shader SPIRV-ASM + OpCapability Shader + OpMemoryModel Logical GLSL450 + OpEntryPoint GLCompute %1 "main" %2 + OpExecutionMode %1 LocalSize 4 1 1 + OpDecorate %3 ArrayStride 4 + OpMemberDecorate %4 0 Offset 0 + OpDecorate %4 BufferBlock + OpDecorate %5 DescriptorSet 0 + OpDecorate %5 Binding 1 + OpDecorate %2 BuiltIn GlobalInvocationId + OpDecorate %6 DescriptorSet 0 + OpDecorate %6 Binding 0 + %7 = OpTypeVoid + %8 = OpTypeFunction %7 + %9 = OpTypeInt 32 1 +%10 = OpTypeInt 32 0 + %3 = OpTypeRuntimeArray %9 + %4 = OpTypeStruct %3 +%11 = OpTypePointer Uniform %4 + %5 = OpVariable %11 Uniform +%12 = OpConstant %9 0 +%13 = OpConstant %10 0 +%14 = OpTypeVector %10 3 +%15 = OpTypePointer Input %14 + %2 = OpVariable %15 Input +%16 = OpTypePointer Input %10 + %6 = OpVariable %11 Uniform +%17 = OpTypePointer Uniform %9 + %1 = OpFunction %7 None %8 +%18 = OpLabel +%19 = OpAccessChain %16 %2 %13 +%20 = OpLoad %10 %19 +%21 = OpAccessChain %17 %6 %12 %20 +%22 = OpLoad %9 %21 +%23 = OpAccessChain %17 %5 %12 %20 + OpStore %23 %22 + OpReturn + OpFunctionEnd +END + +BUFFER buf_in DATA_TYPE uint32 DATA + 20 30 40 50 60 +END + +BUFFER buf_out DATA_TYPE uint32 DATA + 99 99 99 99 99 +END + +PIPELINE compute pipeline + ATTACH mah_shader + + BIND BUFFER buf_in AS storage DESCRIPTOR_SET 0 BINDING 0 + BIND BUFFER buf_out AS storage DESCRIPTOR_SET 0 BINDING 1 +END + +# Only one workgroup. Having only one invocation execute ensures +# there are no race conditions. +DEBUG pipeline 1 1 1 + THREAD GLOBAL_INVOCATION_ID 2 0 0 + EXPECT LOCATION "ComputeShader0.spvasm" 1 "OpCapability Shader" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 2 "OpMemoryModel Logical GLSL450" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 3 "OpEntryPoint GLCompute %1 \"main\" %2" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 4 "OpExecutionMode %1 LocalSize 4 1 1" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 5 "OpDecorate %3 ArrayStride 4" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 6 "OpMemberDecorate %4 0 Offset 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 7 "OpDecorate %4 BufferBlock" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 8 "OpDecorate %5 DescriptorSet 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 9 "OpDecorate %5 Binding 1" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 10 "OpDecorate %2 BuiltIn GlobalInvocationId" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 11 "OpDecorate %6 DescriptorSet 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 12 "OpDecorate %6 Binding 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 13 "%7 = OpTypeVoid" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 14 "%8 = OpTypeFunction %7" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 15 "%9 = OpTypeInt 32 1" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 16 "%10 = OpTypeInt 32 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 17 "%3 = OpTypeRuntimeArray %9" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 18 "%4 = OpTypeStruct %3" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 19 "%11 = OpTypePointer Uniform %4" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 20 "%5 = OpVariable %11 Uniform" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 21 "%12 = OpConstant %9 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 22 "%13 = OpConstant %10 0" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 23 "%14 = OpTypeVector %10 3" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 24 "%15 = OpTypePointer Input %14" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 25 "%2 = OpVariable %15 Input" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 26 "%16 = OpTypePointer Input %10" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 27 "%6 = OpVariable %11 Uniform" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 28 "%17 = OpTypePointer Uniform %9" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 29 "%1 = OpFunction %7 None %8" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 30 "%18 = OpLabel" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 31 "%19 = OpAccessChain %16 %2 %13" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 32 "%20 = OpLoad %10 %19" + STEP_IN + EXPECT LOCAL "%20" EQ 2 + EXPECT LOCATION "ComputeShader0.spvasm" 33 "%21 = OpAccessChain %17 %6 %12 %20" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 34 "%22 = OpLoad %9 %21" + STEP_IN + EXPECT LOCAL "%22" EQ 40 + EXPECT LOCATION "ComputeShader0.spvasm" 35 "%23 = OpAccessChain %17 %5 %12 %20" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 36 "OpStore %23 %22" + STEP_IN + EXPECT LOCATION "ComputeShader0.spvasm" 37 "OpReturn" + CONTINUE + END +END + +EXPECT buf_in IDX 0 EQ 20 30 40 50 60 +EXPECT buf_out IDX 0 EQ 20 30 40 50 99 + diff --git a/tests/run_tests.py b/tests/run_tests.py index 2484be1c3..dff50d2a6 100755 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -54,6 +54,12 @@ ] } +SUPPRESSIONS_DEBUGGER = [ + # Debugger functionality is not ready for testing (yet) + "debugger_hlsl_line_stepping.amber", + "debugger_spirv_line_stepping.amber", +] + SUPPRESSIONS_SWIFTSHADER = [ # Incorrect rendering: github.com/google/amber/issues/727 "draw_array_instanced.vkscript", @@ -183,6 +189,9 @@ def IsSuppressed(self): if not self.use_opencl and is_opencl_test: return True + if base in SUPPRESSIONS_DEBUGGER: + return True + if system in SUPPRESSIONS.keys(): is_system_suppressed = base in SUPPRESSIONS[system] return is_system_suppressed From 18c98b1cb7515feaf9ba8246fbf7071aa93496fe Mon Sep 17 00:00:00 2001 From: Ben Clayton Date: Thu, 30 Jan 2020 14:18:05 +0000 Subject: [PATCH 7/7] Debugger: Restructure amber::debug interfaces Remove the need for callbacks, which unnecessarily compilated the parsing. --- src/amberscript/parser.cc | 27 ++++++---- src/debug.cc | 74 +++++++++++++++------------- src/debug.h | 65 +++++++++++++----------- src/vulkan/engine_vulkan_debugger.cc | 33 +++++++------ 4 files changed, 113 insertions(+), 86 deletions(-) diff --git a/src/amberscript/parser.cc b/src/amberscript/parser.cc index 7eb3b9647..57f9f2e59 100644 --- a/src/amberscript/parser.cc +++ b/src/amberscript/parser.cc @@ -1816,7 +1816,7 @@ Result Parser::ParseDebug() { return res; } - auto dbg = MakeUnique(); + auto dbg = debug::Script::Create(); for (auto token = tokenizer_->NextToken();; token = tokenizer_->NextToken()) { if (token->IsEOL()) continue; @@ -1841,11 +1841,6 @@ Result Parser::ParseDebug() { } Result Parser::ParseDebugThread(debug::Events* dbg) { - Result result; - auto parseThread = [&](debug::Thread* thread) { - result = ParseDebugThreadBody(thread); - }; - auto token = tokenizer_->NextToken(); if (token->AsString() == "GLOBAL_INVOCATION_ID") { uint32_t invocation[3] = {}; @@ -1855,19 +1850,33 @@ Result Parser::ParseDebugThread(debug::Events* dbg) { return Result("expected invocation index"); invocation[i] = token->AsUint32(); } + + auto thread = debug::ThreadScript::Create(); + auto result = ParseDebugThreadBody(thread.get()); + if (!result.IsSuccess()) { + return result; + } + dbg->BreakOnComputeGlobalInvocation(invocation[0], invocation[1], - invocation[2], parseThread); + invocation[2], thread); } else if (token->AsString() == "VERTEX_INDEX") { token = tokenizer_->NextToken(); if (!token->IsInteger()) return Result("expected vertex index"); auto vertex_index = token->AsUint32(); - dbg->BreakOnVertexIndex(vertex_index, parseThread); + + auto thread = debug::ThreadScript::Create(); + auto result = ParseDebugThreadBody(thread.get()); + if (!result.IsSuccess()) { + return result; + } + + dbg->BreakOnVertexIndex(vertex_index, thread); } else { return Result("expected GLOBAL_INVOCATION_ID or VERTEX_INDEX"); } - return result; + return Result(); } Result Parser::ParseDebugThreadBody(debug::Thread* thread) { diff --git a/src/debug.cc b/src/debug.cc index 7d6c2265f..574b62482 100644 --- a/src/debug.cc +++ b/src/debug.cc @@ -14,8 +14,10 @@ #include "src/debug.h" +#include #include #include +#include #include "src/make_unique.h" @@ -24,15 +26,44 @@ namespace debug { namespace { -// ThreadScript is an implementation of amber::debug::Thread that records all -// calls made on it, which can be later replayed using ThreadScript::Run(). -class ThreadScript : public Thread { +class ScriptImpl : public Script { public: - void Run(Thread* thread) { + void Run(Events* e) const override { + for (auto f : sequence_) { + f(e); + } + } + + void BreakOnComputeGlobalInvocation( + uint32_t x, + uint32_t y, + uint32_t z, + const std::shared_ptr& thread) override { + sequence_.emplace_back([=](Events* events) { + events->BreakOnComputeGlobalInvocation(x, y, z, thread); + }); + } + + void BreakOnVertexIndex( + uint32_t index, + const std::shared_ptr& thread) override { + sequence_.emplace_back( + [=](Events* events) { events->BreakOnVertexIndex(index, thread); }); + } + + private: + using Event = std::function; + std::vector sequence_; +}; + +class ThreadScriptImpl : public ThreadScript { + public: + void Run(Thread* thread) const override { for (auto f : sequence_) { f(thread); } } + // Thread compliance void StepOver() override { sequence_.emplace_back([](Thread* t) { t->StepOver(); }); @@ -77,38 +108,15 @@ class ThreadScript : public Thread { Thread::~Thread() = default; Events::~Events() = default; +ThreadScript::~ThreadScript() = default; +Script::~Script() = default; -void Script::Run(Events* e) { - for (auto f : sequence_) { - f(e); - } +std::shared_ptr ThreadScript::Create() { + return std::make_shared(); } -void Script::BreakOnComputeGlobalInvocation(uint32_t x, - uint32_t y, - uint32_t z, - const OnThread& callback) { - auto script = std::make_shared(); - callback(script.get()); // Record - - sequence_.emplace_back([=](Events* events) { - events->BreakOnComputeGlobalInvocation(x, y, z, [=](Thread* thread) { - script->Run(thread); // Replay - }); - }); -} - -void Script::BreakOnVertexIndex(uint32_t index, const OnThread& callback) { - // std::make_shared is used here instead of MakeUnique as std::function is - // copyable, and cannot capture move-only values. - auto script = std::make_shared(); - callback(script.get()); // Record - - sequence_.emplace_back([=](Events* events) { - events->BreakOnVertexIndex(index, [=](Thread* thread) { - script->Run(thread); // Replay - }); - }); +std::unique_ptr