Add support for VIRTUAL_FILE
`VIRTUAL_FILE` declares a file in a new virtual file system.
These files can now be referenced by the `SHADER` commands instead of using inline source.
The DXC compiler will first look in the virtual file system for any `#include`s before falling back to the standard file system.
This allows us to write tests that exercise the compiler and debugger handling of multiple files.
Added `relative_includes_hlsl.amber` which checks that DXC correctly includes relative to the current file, not the root file.
diff --git a/Android.mk b/Android.mk
index b373245..ae83c26 100644
--- a/Android.mk
+++ b/Android.mk
@@ -47,6 +47,7 @@
src/type_parser.cc \
src/value.cc \
src/verifier.cc \
+ src/virtual_file_store.cc \
src/vkscript/command_parser.cc \
src/vkscript/datum_type_parser.cc \
src/vkscript/parser.cc \
diff --git a/docs/amber_script.md b/docs/amber_script.md
index 56430f7..f1540f9 100644
--- a/docs/amber_script.md
+++ b/docs/amber_script.md
@@ -68,8 +68,63 @@
SET ENGINE_DATA {engine data variable} {value}*
```
+### Virtual File Store
+
+Each amber script contains a virtual file system that can store files of textual
+data. This lets you bundle multiple source files into a single, hermetic amber
+script file.
+
+Virtual files are declared using the `VIRTUAL_FILE` command:
+
+```groovy
+VIRTUAL_FILE {path}
+ {file-content}
+END
+```
+
+Paths must be unique.
+
+Shaders can directly reference these virtual files for their source. \
+HLSL shaders that `#include` other `.hlsl` files will first check the virtual
+file system, before falling back to the standard file system.
+
### Shaders
+Shader programs are declared using the `SHADER` command. \
+Shaders can be declared as `PASSTHROUGH`, with inlined source or using source
+from a `VIRTUAL_FILE`.
+
+Pass-through shader:
+
+```groovy
+# Creates a passthrough vertex shader. The shader passes the vec4 at input
+# location 0 through to the `gl_Position`.
+SHADER vertex {shader_name} PASSTHROUGH
+```
+
+Shader using inlined source:
+
+```groovy
+# Creates a shader of |shader_type| with the given |shader_name|. The shader
+# will be of |shader_format|. The shader source then follows and is terminated
+# with the |END| tag.
+SHADER {shader_type} {shader_name} {shader_format}
+{shader_source}
+END
+```
+
+Shader using source from `VIRTUAL_FILE`:
+
+```groovy
+# Creates a shader of |shader_type| with the given |shader_name|. The shader
+# will be of |shader_format|. The shader will use the virtual file with |path|.
+SHADER {shader_type} {shader_name} {shader_format} VIRTUAL_FILE {path}
+```
+
+`{shader_name}` is used to identify the shader to attach to `PIPELINE`s,
+
+`{shader_type}` and `{shader_format}` are described below:
+
#### Shader Type
* `vertex`
* `fragment`
@@ -92,24 +147,11 @@
#### Shader Format
* `GLSL` (with glslang)
- * `HLSL` (with dxc or glslang if dxc disabled) -- future
+ * `HLSL` (with dxc or glslang if dxc disabled)
* `SPIRV-ASM` (with spirv-as)
* `SPIRV-HEX` (decoded straight to SPIR-V)
* `OPENCL-C` (with clspv)
-```groovy
-# Creates a passthrough vertex shader. The shader passes the vec4 at input
-# location 0 through to the `gl_Position`.
-SHADER vertex {shader_name} PASSTHROUGH
-
-# Creates a shader of |shader_type| with the given |shader_name|. The shader
-# will be of |shader_format|. The shader should then be inlined before the
-# |END| tag.
-SHADER {shader_type} {shader_name} {shader_format}
-...
-END
-```
-
### Buffers
An AmberScript buffer represents a set of contiguous bits. This can be used for
@@ -471,7 +513,7 @@
```groovy
# Run the given |pipeline_name| which must be a `graphics` pipeline. The
-# grid at |x|, |y|, |width|x|height|, |columns|x|rows| will be rendered.
+# grid at |x|, |y|, |width|x|height|, |columns|x|rows| will be rendered.
# Ignores VERTEX_DATA and INDEX_DATA on the given pipeline.
# For columns, rows of (5, 4) a total of 5*4=20 rectangles will be drawn.
RUN {pipeline_name} \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a50c33c..c18f390 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -39,6 +39,7 @@
type_parser.cc
value.cc
verifier.cc
+ virtual_file_store.cc
vkscript/command_parser.cc
vkscript/datum_type_parser.cc
vkscript/parser.cc
@@ -163,6 +164,7 @@
type_parser_test.cc
type_test.cc
verifier_test.cc
+ virtual_file_store_test.cc
vkscript/command_parser_test.cc
vkscript/datum_type_parser_test.cc
vkscript/parser_test.cc
diff --git a/src/amberscript/parser.cc b/src/amberscript/parser.cc
index 60292b0..10162b8 100644
--- a/src/amberscript/parser.cc
+++ b/src/amberscript/parser.cc
@@ -200,6 +200,8 @@
r = ParseStruct();
} else if (tok == "SAMPLER") {
r = ParseSampler();
+ } else if (tok == "VIRTUAL_FILE") {
+ r = ParseVirtualFile();
} else {
r = Result("unknown token: " + tok);
}
@@ -370,19 +372,42 @@
shader->SetFormat(format);
- r = ValidateEndOfStatement("SHADER command");
- if (!r.IsSuccess())
- return r;
+ token = tokenizer_->PeekNextToken();
+ if (token->IsIdentifier() && token->AsString() == "VIRTUAL_FILE") {
+ tokenizer_->NextToken(); // Skip VIRTUAL_FILE
- std::string data = tokenizer_->ExtractToNext("END");
- if (data.empty())
- return Result("SHADER must not be empty");
+ token = tokenizer_->NextToken();
+ if (!token->IsIdentifier() && !token->IsString())
+ return Result("expected virtual file path after VIRTUAL_FILE");
- shader->SetData(data);
+ r = ValidateEndOfStatement("SHADER command");
+ if (!r.IsSuccess())
+ return r;
- token = tokenizer_->NextToken();
- if (!token->IsIdentifier() || token->AsString() != "END")
- return Result("SHADER missing END command");
+ auto path = token->AsString();
+
+ std::string data;
+ r = script_->GetVirtualFile(path, &data);
+ if (!r.IsSuccess()) {
+ return r;
+ }
+
+ shader->SetData(data);
+ } else {
+ r = ValidateEndOfStatement("SHADER command");
+ if (!r.IsSuccess())
+ return r;
+
+ std::string data = tokenizer_->ExtractToNext("END");
+ if (data.empty())
+ return Result("SHADER must not be empty");
+
+ shader->SetData(data);
+
+ token = tokenizer_->NextToken();
+ if (!token->IsIdentifier() || token->AsString() != "END")
+ return Result("SHADER missing END command");
+ }
r = script_->AddShader(std::move(shader));
if (!r.IsSuccess())
@@ -2896,5 +2921,25 @@
return {};
}
+Result Parser::ParseVirtualFile() {
+ auto token = tokenizer_->NextToken();
+ if (!token->IsIdentifier() && !token->IsString())
+ return Result("invalid virtual file path");
+
+ auto path = token->AsString();
+
+ auto r = ValidateEndOfStatement("VIRTUAL_FILE command");
+ if (!r.IsSuccess())
+ return r;
+
+ auto data = tokenizer_->ExtractToNext("END");
+
+ token = tokenizer_->NextToken();
+ if (!token->IsIdentifier() || token->AsString() != "END")
+ return Result("VIRTUAL_FILE missing END command");
+
+ return script_->AddVirtualFile(path, data);
+}
+
} // namespace amberscript
} // namespace amber
diff --git a/src/amberscript/parser.h b/src/amberscript/parser.h
index 8f62e90..524cb43 100644
--- a/src/amberscript/parser.h
+++ b/src/amberscript/parser.h
@@ -97,6 +97,8 @@
Format* fmt,
std::vector<Value>* values);
+ Result ParseVirtualFile();
+
std::unique_ptr<Tokenizer> tokenizer_;
std::vector<std::unique_ptr<Command>> command_list_;
};
diff --git a/src/amberscript/parser_shader_test.cc b/src/amberscript/parser_shader_test.cc
index 4c2ca69..07024a2 100644
--- a/src/amberscript/parser_shader_test.cc
+++ b/src/amberscript/parser_shader_test.cc
@@ -235,6 +235,54 @@
EXPECT_EQ("2: extra parameters after SHADER command: INVALID", r.Error());
}
+TEST_F(AmberScriptParserTest, ShaderVirtualFile) {
+ std::string in = R"(#!amber
+VIRTUAL_FILE my_shader.hlsl
+My shader source
+END
+
+SHADER vertex my_shader HLSL VIRTUAL_FILE my_shader.hlsl
+)";
+
+ Parser parser;
+ Result r = parser.Parse(in);
+ ASSERT_EQ(r.Error(), "");
+
+ auto script = parser.GetScript();
+ auto shader = script->GetShader("my_shader");
+ ASSERT_TRUE(shader != nullptr);
+ auto source = shader->GetData();
+ ASSERT_EQ("My shader source\n", shader->GetData());
+}
+
+TEST_F(AmberScriptParserTest, VirtualFileDuplicatePath) {
+ std::string in = R"(#!amber
+VIRTUAL_FILE my.file
+Blah
+END
+
+VIRTUAL_FILE my.file
+Blah
+END
+)";
+
+ Parser parser;
+ Result r = parser.Parse(in);
+ ASSERT_EQ(r.Error(), "8: Virtual file 'my.file' already declared");
+}
+
+TEST_F(AmberScriptParserTest, VirtualFileEmptyPath) {
+ std::string in = R"(#!amber
+VIRTUAL_FILE ""
+Blah
+END
+)";
+
+ Parser parser;
+ Result r = parser.Parse(in);
+ ASSERT_EQ(r.Error(), "4: Virtual file path was empty");
+}
+
struct ShaderTypeData {
const char* name;
ShaderType type;
@@ -315,6 +363,7 @@
EXPECT_EQ(test_data.format, shader->GetFormat());
EXPECT_EQ(shader_result, shader->GetData());
}
+
INSTANTIATE_TEST_SUITE_P(
AmberScriptParserTestsShaderFormat,
AmberScriptParserShaderFormatTest,
diff --git a/src/dxc_helper.cc b/src/dxc_helper.cc
index b47616a..8ab2ebd 100644
--- a/src/dxc_helper.cc
+++ b/src/dxc_helper.cc
@@ -18,6 +18,7 @@
#include <sstream>
#include "src/platform.h"
+#include "src/virtual_file_store.h"
#if AMBER_PLATFORM_WINDOWS
#pragma warning(push)
@@ -25,8 +26,6 @@
#pragma warning(disable : 4003)
#endif // AMBER_PLATFORM_WINDOWS
-// clang-format off
-// The order here matters, so don't reformat.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-id-macro"
#pragma clang diagnostic ignored "-Wextra-semi"
@@ -40,22 +39,25 @@
#pragma clang diagnostic ignored "-Wdocumentation-unknown-command"
#pragma clang diagnostic ignored "-Wundef"
#pragma clang diagnostic ignored "-Wunused-function"
+#pragma clang diagnostic ignored "-Wunused-parameter"
+#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-parameter"
#ifndef __STDC_LIMIT_MACROS
#define __STDC_LIMIT_MACROS
#endif // __STDC_LIMIT_MACROS
#ifndef __STDC_CONSTANT_MACROS
#define __STDC_CONSTANT_MACROS
#endif // __STDC_CONSTANT_MACROS
+
+// clang-format off
+// The order here matters, so don't reformat.
#include "dxc/Support/Global.h"
#include "dxc/Support/HLSLOptions.h"
#include "dxc/dxcapi.h"
-#pragma clang diagnostic pop
+#include "dxc/Support/microcom.h"
// clang-format on
-#if AMBER_PLATFORM_WINDOWS
-#pragma warning(pop)
-#endif // AMBER_PLATFORM_WINDOWS
-
namespace amber {
namespace dxchelper {
namespace {
@@ -79,12 +81,60 @@
memcpy(binaryWords->data(), binaryStr.data(), binaryStr.size());
}
+class IncludeHandler : public IDxcIncludeHandler {
+ public:
+ IncludeHandler(const VirtualFileStore* file_store,
+ IDxcLibrary* dxc_lib,
+ IDxcIncludeHandler* fallback)
+ : file_store_(file_store), dxc_lib_(dxc_lib), fallback_(fallback) {}
+
+ HRESULT STDMETHODCALLTYPE LoadSource(LPCWSTR pFilename,
+ IDxcBlob** ppIncludeSource) override {
+ std::wstring wide_path(pFilename);
+ std::string path = std::string(wide_path.begin(), wide_path.end());
+
+ std::string content;
+ Result r = file_store_->Get(path, &content);
+ if (r.IsSuccess()) {
+ IDxcBlobEncoding* source;
+ auto res = dxc_lib_->CreateBlobWithEncodingOnHeapCopy(
+ content.data(), static_cast<uint32_t>(content.size()), CP_UTF8,
+ &source);
+ if (res != S_OK) {
+ DxcCleanupThreadMalloc();
+ return res;
+ }
+ *ppIncludeSource = source;
+ return S_OK;
+ }
+
+ return fallback_->LoadSource(pFilename, ppIncludeSource);
+ }
+
+ HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
+ void** ppvObject) override {
+ return DoBasicQueryInterface<IDxcIncludeHandler>(this, iid, ppvObject);
+ }
+
+ private:
+ const VirtualFileStore* const file_store_;
+ IDxcLibrary* const dxc_lib_;
+ IDxcIncludeHandler* const fallback_;
+};
+
+#pragma GCC diagnostic pop
+#pragma clang diagnostic pop
+#if AMBER_PLATFORM_WINDOWS
+#pragma warning(pop)
+#endif // AMBER_PLATFORM_WINDOWS
+
} // namespace
Result Compile(const std::string& src,
const std::string& entry,
const std::string& profile,
const std::string& spv_env,
+ const VirtualFileStore* virtual_files,
std::vector<uint32_t>* generated_binary) {
if (hlsl::options::initHlslOptTable()) {
DxcCleanupThreadMalloc();
@@ -106,12 +156,15 @@
return Result("DXC compile failure: CreateBlobFromFile");
}
- IDxcIncludeHandler* include_handler;
- if (dxc_lib->CreateIncludeHandler(&include_handler) < 0) {
+ IDxcIncludeHandler* fallback_include_handler;
+ if (dxc_lib->CreateIncludeHandler(&fallback_include_handler) < 0) {
DxcCleanupThreadMalloc();
return Result("DXC compile failure: CreateIncludeHandler");
}
+ IDxcIncludeHandler* include_handler =
+ new IncludeHandler(virtual_files, dxc_lib, fallback_include_handler);
+
IDxcCompiler* compiler;
if (DxcCreateInstance(CLSID_DxcCompiler, __uuidof(IDxcCompiler),
reinterpret_cast<void**>(&compiler)) < 0) {
diff --git a/src/dxc_helper.h b/src/dxc_helper.h
index 4edb55d..22082ce 100644
--- a/src/dxc_helper.h
+++ b/src/dxc_helper.h
@@ -21,6 +21,9 @@
#include "amber/result.h"
namespace amber {
+
+class VirtualFileStore;
+
namespace dxchelper {
// Passes the HLSL source code to the DXC compiler with SPIR-V CodeGen.
@@ -29,6 +32,7 @@
const std::string& entry_str,
const std::string& profile_str,
const std::string& spv_env,
+ const VirtualFileStore* virtual_files,
std::vector<uint32_t>* generated_binary);
} // namespace dxchelper
diff --git a/src/executor.cc b/src/executor.cc
index 5ff76d2..b0fb585 100644
--- a/src/executor.cc
+++ b/src/executor.cc
@@ -35,7 +35,8 @@
for (auto& pipeline : script->GetPipelines()) {
for (auto& shader_info : pipeline->GetShaders()) {
ShaderCompiler sc(script->GetSpvTargetEnv(),
- options->disable_spirv_validation);
+ options->disable_spirv_validation,
+ script->GetVirtualFiles());
Result r;
std::vector<uint32_t> data;
diff --git a/src/script.cc b/src/script.cc
index d262821..b6dd743 100644
--- a/src/script.cc
+++ b/src/script.cc
@@ -14,11 +14,12 @@
#include "src/script.h"
+#include "src/make_unique.h"
#include "src/type_parser.h"
namespace amber {
-Script::Script() = default;
+Script::Script() : virtual_files_(MakeUnique<VirtualFileStore>()) {}
Script::~Script() = default;
diff --git a/src/script.h b/src/script.h
index b8f881b..e6bd2ee 100644
--- a/src/script.h
+++ b/src/script.h
@@ -31,6 +31,7 @@
#include "src/pipeline.h"
#include "src/sampler.h"
#include "src/shader.h"
+#include "src/virtual_file_store.h"
namespace amber {
@@ -222,6 +223,22 @@
return it == name_to_type_.end() ? nullptr : it->second.get();
}
+ // Returns the virtual file store.
+ VirtualFileStore* GetVirtualFiles() const { return virtual_files_.get(); }
+
+ /// Adds the virtual file with content |content| to the virtual file path
+ /// |path|. If there's already a virtual file with the given path, an error is
+ /// returned.
+ Result AddVirtualFile(const std::string& path, const std::string& content) {
+ return virtual_files_->Add(path, content);
+ }
+
+ /// Look up the virtual file by path. If the file was found, the content is
+ /// assigned to content.
+ Result GetVirtualFile(const std::string& path, std::string* content) const {
+ return virtual_files_->Get(path, content);
+ }
+
type::Type* ParseType(const std::string& str);
private:
@@ -245,6 +262,7 @@
std::vector<std::unique_ptr<Pipeline>> pipelines_;
std::vector<std::unique_ptr<type::Type>> types_;
std::vector<std::unique_ptr<Format>> formats_;
+ std::unique_ptr<VirtualFileStore> virtual_files_;
};
} // namespace amber
diff --git a/src/shader_compiler.cc b/src/shader_compiler.cc
index 5c6944b..4eb328b 100644
--- a/src/shader_compiler.cc
+++ b/src/shader_compiler.cc
@@ -48,8 +48,15 @@
ShaderCompiler::ShaderCompiler() = default;
ShaderCompiler::ShaderCompiler(const std::string& env,
- bool disable_spirv_validation)
- : spv_env_(env), disable_spirv_validation_(disable_spirv_validation) {}
+ bool disable_spirv_validation,
+ VirtualFileStore* virtual_files)
+ : spv_env_(env),
+ disable_spirv_validation_(disable_spirv_validation),
+ virtual_files_(virtual_files) {
+ // Do not warn about virtual_files_ not being used.
+ // This is conditionally used based on preprocessor defines.
+ (void)virtual_files_;
+}
ShaderCompiler::~ShaderCompiler() = default;
@@ -269,7 +276,7 @@
return Result("Unknown shader type");
return dxchelper::Compile(shader->GetData(), "main", target, spv_env_,
- result);
+ virtual_files_, result);
}
#else
Result ShaderCompiler::CompileHlsl(const Shader*,
diff --git a/src/shader_compiler.h b/src/shader_compiler.h
index fdf7430..e4caa62 100644
--- a/src/shader_compiler.h
+++ b/src/shader_compiler.h
@@ -23,6 +23,7 @@
#include "amber/result.h"
#include "src/pipeline.h"
#include "src/shader.h"
+#include "src/virtual_file_store.h"
namespace amber {
@@ -30,7 +31,9 @@
class ShaderCompiler {
public:
ShaderCompiler();
- ShaderCompiler(const std::string& env, bool disable_spirv_validation);
+ ShaderCompiler(const std::string& env,
+ bool disable_spirv_validation,
+ VirtualFileStore* virtual_files);
~ShaderCompiler();
/// Returns a result code and a compilation of the given shader.
@@ -61,6 +64,7 @@
std::string spv_env_;
bool disable_spirv_validation_ = false;
+ VirtualFileStore* virtual_files_ = nullptr;
};
// Parses the SPIR-V environment string, and returns the corresponding
diff --git a/src/virtual_file_store.cc b/src/virtual_file_store.cc
new file mode 100644
index 0000000..a14d64c
--- /dev/null
+++ b/src/virtual_file_store.cc
@@ -0,0 +1,48 @@
+// 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/virtual_file_store.h"
+
+namespace amber {
+namespace {
+
+bool HasPrefix(const std::string& str, const std::string& prefix) {
+ return str.compare(0, prefix.size(), prefix) == 0;
+}
+
+std::string TrimPrefix(const std::string& str, const std::string& prefix) {
+ return HasPrefix(str, prefix) ? str.substr(prefix.length()) : str;
+}
+
+std::string ReplaceAll(std::string str,
+ const std::string& substr,
+ const std::string& replacement) {
+ size_t pos = 0;
+ while ((pos = str.find(substr, pos)) != std::string::npos) {
+ str.replace(pos, substr.length(), replacement);
+ pos += replacement.length();
+ }
+ return str;
+}
+
+} // namespace
+
+std::string VirtualFileStore::GetCanonical(const std::string& path) {
+ auto canonical = path;
+ canonical = ReplaceAll(canonical, "\\", "/");
+ canonical = TrimPrefix(canonical, "./");
+ return canonical;
+}
+
+} // namespace amber
diff --git a/src/virtual_file_store.h b/src/virtual_file_store.h
new file mode 100644
index 0000000..f3e3d65
--- /dev/null
+++ b/src/virtual_file_store.h
@@ -0,0 +1,75 @@
+// 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_VIRTUAL_FILE_STORE_H_
+#define SRC_VIRTUAL_FILE_STORE_H_
+
+#include <cassert>
+#include <string>
+#include <unordered_map>
+
+#include "amber/result.h"
+
+namespace amber {
+
+/// Stores a number of virtual files by path.
+class VirtualFileStore {
+ public:
+ /// Return the path sanitized into a canonical form.
+ static std::string GetCanonical(const std::string& path);
+
+ /// Adds the virtual file with content |content| to the virtual file path
+ /// |path|. If there's already a virtual file with the given path, an error is
+ /// returned.
+ Result Add(const std::string& path, const std::string& content) {
+ if (path.length() == 0) {
+ return Result("Virtual file path was empty");
+ }
+
+ auto canonical = GetCanonical(path);
+
+ auto it = files_by_path_.find(canonical);
+ if (it != files_by_path_.end()) {
+ return Result("Virtual file '" + path + "' already declared");
+ }
+ files_by_path_.emplace(canonical, content);
+ return {};
+ }
+
+ /// Look up the virtual file by path. If the file was found, the content is
+ /// assigned to content.
+ Result Get(const std::string& path, std::string* content) const {
+ assert(content);
+
+ if (path.length() == 0) {
+ return Result("Virtual file path was empty");
+ }
+
+ auto canonical = GetCanonical(path);
+
+ auto it = files_by_path_.find(canonical);
+ if (it == files_by_path_.end()) {
+ return Result("Virtual file '" + path + "' not found");
+ }
+ *content = it->second;
+ return {};
+ }
+
+ private:
+ std::unordered_map<std::string, std::string> files_by_path_;
+};
+
+} // namespace amber
+
+#endif // SRC_VIRTUAL_FILE_STORE_H_
diff --git a/src/virtual_file_store_test.cc b/src/virtual_file_store_test.cc
new file mode 100644
index 0000000..89e0320
--- /dev/null
+++ b/src/virtual_file_store_test.cc
@@ -0,0 +1,49 @@
+// 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/virtual_file_store.h"
+
+#include "gtest/gtest.h"
+
+namespace amber {
+
+TEST(VirtualFileStore, Canonical) {
+ ASSERT_EQ("a/b/c.e", VirtualFileStore::GetCanonical("a/b/c.e"));
+ ASSERT_EQ("a/b.c.e", VirtualFileStore::GetCanonical("a/b.c.e"));
+ ASSERT_EQ("a/b/c.e", VirtualFileStore::GetCanonical("a\\b\\c.e"));
+ ASSERT_EQ("a/b/c.e", VirtualFileStore::GetCanonical("./a/b/c.e"));
+}
+
+TEST(VirtualFileStore, AddGet) {
+ VirtualFileStore store;
+ store.Add("a/file.1", "File 1");
+ store.Add("./file.2", "File 2");
+ store.Add("b\\file.3", "File 3");
+
+ std::string content;
+ ASSERT_TRUE(store.Get("a/file.1", &content).IsSuccess());
+ ASSERT_EQ("File 1", content);
+
+ ASSERT_TRUE(store.Get("./file.2", &content).IsSuccess());
+ ASSERT_EQ("File 2", content);
+
+ ASSERT_TRUE(store.Get("b\\file.3", &content).IsSuccess());
+ ASSERT_EQ("File 3", content);
+
+ content = "<not-assigned>";
+ ASSERT_FALSE(store.Get("missing.file", &content).IsSuccess());
+ ASSERT_EQ("<not-assigned>", content);
+}
+
+} // namespace amber
diff --git a/tests/cases/relative_includes_hlsl.amber b/tests/cases/relative_includes_hlsl.amber
new file mode 100644
index 0000000..dd91eea
--- /dev/null
+++ b/tests/cases/relative_includes_hlsl.amber
@@ -0,0 +1,56 @@
+#!amber
+# Copyright 2019 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.
+
+
+VIRTUAL_FILE "relative.hlsl"
+#error "Wrong include picked!"
+END
+
+VIRTUAL_FILE "subdir/relative.hlsl"
+// Correct include!
+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;
+}
+END
+
+VIRTUAL_FILE "subdir/include.hlsl"
+#include "relative.hlsl"
+END
+
+VIRTUAL_FILE "main.hlsl"
+#include "subdir/include.hlsl"
+END
+
+SHADER vertex vtex_shader HLSL VIRTUAL_FILE main.hlsl
+
+SHADER fragment frag_shader HLSL
+float4 main(float4 color : COLOR) : SV_TARGET {
+ return color;
+}
+END
+
+PIPELINE graphics pipeline
+ ATTACH vtex_shader
+ ATTACH frag_shader
+END
diff --git a/tests/run_tests.py b/tests/run_tests.py
index d713a26..d908998 100755
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -99,6 +99,7 @@
DXC_CASES = [
"draw_triangle_list_hlsl.amber",
+ "relative_includes_hlsl.amber",
]
SUPPRESSIONS_DAWN = [