Implement support for pre-built SPIR-V binaries

vk-build-programs utility can be used to build and store SPIR-V
binaries.

If glslang is not available, test framework will attempt to load
pre-built binaries from vulkan/prebuilt directory.

scripts/build_vulkan_programs.py can be used to populate
data/vulkan/prebuilt directory with SPIR-V binaries.

Change-Id: I1e998bba0e0021d0e5b6da35ed53c54a61207bff
diff --git a/data/vulkan/.gitignore b/data/vulkan/.gitignore
new file mode 100644
index 0000000..21ba210
--- /dev/null
+++ b/data/vulkan/.gitignore
@@ -0,0 +1 @@
+prebuilt
diff --git a/framework/vulkan/CMakeLists.txt b/framework/vulkan/CMakeLists.txt
index b3380b5..e615e4a 100644
--- a/framework/vulkan/CMakeLists.txt
+++ b/framework/vulkan/CMakeLists.txt
@@ -19,6 +19,8 @@
 	vkDeviceUtil.hpp
 	vkGlslToSpirV.cpp
 	vkGlslToSpirV.hpp
+	vkBinaryRegistry.cpp
+	vkBinaryRegistry.hpp
 	)
 
 # \note Code interfacing with glslang needs to include third-party headers
diff --git a/framework/vulkan/vkBinaryRegistry.cpp b/framework/vulkan/vkBinaryRegistry.cpp
new file mode 100644
index 0000000..a43864a
--- /dev/null
+++ b/framework/vulkan/vkBinaryRegistry.cpp
@@ -0,0 +1,100 @@
+/*-------------------------------------------------------------------------
+ * drawElements Quality Program Vulkan Utilities
+ * -----------------------------------------------
+ *
+ * Copyright 2015 The Android Open Source Project
+ *
+ * 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.
+ *
+ *//*!
+ * \file
+ * \brief Program binary registry.
+ *//*--------------------------------------------------------------------*/
+
+#include "vkBinaryRegistry.hpp"
+#include "tcuResource.hpp"
+#include "deFilePath.hpp"
+
+#include <fstream>
+
+namespace vk
+{
+
+using std::string;
+using std::vector;
+
+static string getProgramFileName (const ProgramIdentifier& id)
+{
+	// \todo [2015-06-26 pyry] Sanitize progName
+	return id.testCasePath + "." + id.programName + ".spirv";
+}
+
+// BinaryRegistryWriter
+
+BinaryRegistryWriter::BinaryRegistryWriter (const std::string& dstPath)
+	: m_dstPath(dstPath)
+{
+}
+
+BinaryRegistryWriter::~BinaryRegistryWriter (void)
+{
+}
+
+void BinaryRegistryWriter::storeProgram (const ProgramIdentifier& id, const ProgramBinary& binary)
+{
+	const string	fullPath	= de::FilePath::join(m_dstPath, getProgramFileName(id)).getPath();
+	std::ofstream	out			(fullPath.c_str(), std::ios_base::binary);
+
+	if (!out.is_open() || !out.good())
+		throw tcu::Exception("Failed to open " + fullPath);
+
+	out.write((const char*)binary.getBinary(), binary.getSize());
+	out.close();
+}
+
+// BinaryRegistryReader
+
+BinaryRegistryReader::BinaryRegistryReader (const tcu::Archive& archive, const std::string& srcPath)
+	: m_archive	(archive)
+	, m_srcPath	(srcPath)
+{
+}
+
+BinaryRegistryReader::~BinaryRegistryReader (void)
+{
+}
+
+ProgramBinary* BinaryRegistryReader::loadProgram (const ProgramIdentifier& id) const
+{
+	const string	fullPath	= de::FilePath::join(m_srcPath, getProgramFileName(id)).getPath();
+
+	try
+	{
+		de::UniquePtr<tcu::Resource>	progRes		(m_archive.getResource(fullPath.c_str()));
+		const int						progSize	= progRes->getSize();
+		vector<deUint8>					bytes		(progSize);
+
+		TCU_CHECK_INTERNAL(!bytes.empty());
+
+		progRes->read(&bytes[0], progSize);
+
+		return new ProgramBinary(vk::PROGRAM_FORMAT_SPIRV, bytes.size(), &bytes[0]);
+	}
+	catch (const tcu::ResourceError&)
+	{
+		throw ProgramNotFoundException(id);
+	}
+}
+
+
+} // vk
diff --git a/framework/vulkan/vkBinaryRegistry.hpp b/framework/vulkan/vkBinaryRegistry.hpp
new file mode 100644
index 0000000..500b2ed
--- /dev/null
+++ b/framework/vulkan/vkBinaryRegistry.hpp
@@ -0,0 +1,85 @@
+#ifndef _VKBINARYREGISTRY_HPP
+#define _VKBINARYREGISTRY_HPP
+/*-------------------------------------------------------------------------
+ * drawElements Quality Program Vulkan Utilities
+ * -----------------------------------------------
+ *
+ * Copyright 2015 The Android Open Source Project
+ *
+ * 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.
+ *
+ *//*!
+ * \file
+ * \brief Program binary registry.
+ *//*--------------------------------------------------------------------*/
+
+#include "vkDefs.hpp"
+#include "vkPrograms.hpp"
+
+namespace tcu
+{
+class Archive;
+}
+
+namespace vk
+{
+
+struct ProgramIdentifier
+{
+	std::string		testCasePath;
+	std::string		programName;
+
+	ProgramIdentifier (const std::string& testCasePath_, const std::string& programName_)
+		: testCasePath	(testCasePath_)
+		, programName	(programName_)
+	{
+	}
+};
+
+class ProgramNotFoundException : public tcu::ResourceError
+{
+public:
+	ProgramNotFoundException (const ProgramIdentifier& id)
+		: tcu::ResourceError("Program " + id.testCasePath + " / '" + id.programName + "' not found")
+	{
+	}
+};
+
+class BinaryRegistryReader
+{
+public:
+						BinaryRegistryReader	(const tcu::Archive& archive, const std::string& srcPath);
+						~BinaryRegistryReader	(void);
+
+	ProgramBinary*		loadProgram				(const ProgramIdentifier& id) const;
+
+private:
+	const tcu::Archive&	m_archive;
+	const std::string&	m_srcPath;
+};
+
+class BinaryRegistryWriter
+{
+public:
+						BinaryRegistryWriter	(const std::string& dstPath);
+						~BinaryRegistryWriter	(void);
+
+	void				storeProgram			(const ProgramIdentifier& id, const ProgramBinary& binary);
+
+private:
+	const std::string	m_dstPath;
+};
+
+} // vk
+
+#endif // _VKBINARYREGISTRY_HPP
diff --git a/modules/vulkan/CMakeLists.txt b/modules/vulkan/CMakeLists.txt
index 4af36bd..a683732 100644
--- a/modules/vulkan/CMakeLists.txt
+++ b/modules/vulkan/CMakeLists.txt
@@ -40,3 +40,5 @@
 endif ()
 
 add_deqp_module(deqp-vk "${DEQP_VK_SRCS}" "${DEQP_VK_LIBS}" vktTestPackageEntry.cpp)
+
+add_data_dir(deqp-vk ../../data/vulkan	vulkan)
diff --git a/modules/vulkan/vktBuildPrograms.cpp b/modules/vulkan/vktBuildPrograms.cpp
index c6593f8..38c77b1 100644
--- a/modules/vulkan/vktBuildPrograms.cpp
+++ b/modules/vulkan/vktBuildPrograms.cpp
@@ -18,7 +18,7 @@
  *
  *//*!
  * \file
- * \brief
+ * \brief Utility for pre-compiling source programs to SPIR-V
  *//*--------------------------------------------------------------------*/
 
 #include "tcuDefs.hpp"
@@ -29,8 +29,11 @@
 #include "tcuTestHierarchyIterator.hpp"
 #include "deUniquePtr.hpp"
 #include "vkPrograms.hpp"
+#include "vkBinaryRegistry.hpp"
 #include "vktTestCase.hpp"
 #include "vktTestPackage.hpp"
+#include "deUniquePtr.hpp"
+#include "deCommandLine.hpp"
 
 #include <iostream>
 
@@ -48,11 +51,35 @@
 	return new tcu::TestPackageRoot(testCtx, children);
 }
 
-void buildPrograms (tcu::TestContext& testCtx)
+enum BuildMode
+{
+	BUILDMODE_BUILD = 0,
+	BUILDMODE_VERIFY,
+
+	BUILDMODE_LAST
+};
+
+struct BuildStats
+{
+	int		numSucceeded;
+	int		numFailed;
+
+	BuildStats (void)
+		: numSucceeded	(0)
+		, numFailed		(0)
+	{
+	}
+};
+
+BuildStats buildPrograms (tcu::TestContext& testCtx, const std::string& dstPath, BuildMode mode)
 {
 	const UniquePtr<tcu::TestPackageRoot>	root		(createRoot(testCtx));
 	tcu::DefaultHierarchyInflater			inflater	(testCtx);
 	tcu::TestHierarchyIterator				iterator	(*root, inflater, testCtx.getCommandLine());
+	const tcu::DirArchive					srcArchive	(dstPath.c_str());
+	UniquePtr<vk::BinaryRegistryWriter>		writer		(mode == BUILDMODE_BUILD	? new vk::BinaryRegistryWriter(dstPath)			: DE_NULL);
+	UniquePtr<vk::BinaryRegistryReader>		reader		(mode == BUILDMODE_VERIFY	? new vk::BinaryRegistryReader(srcArchive, "")	: DE_NULL);
+	BuildStats								stats;
 
 	while (iterator.getState() != tcu::TestHierarchyIterator::STATE_FINISHED)
 	{
@@ -60,46 +87,107 @@
 			tcu::isTestNodeTypeExecutable(iterator.getNode()->getNodeType()))
 		{
 			const TestCase* const		testCase	= dynamic_cast<TestCase*>(iterator.getNode());
-			const string				path		= iterator.getNodePath();
+			const string				casePath	= iterator.getNodePath();
 			vk::SourceCollection		progs;
 
-			tcu::print("%s\n", path.c_str());
+			tcu::print("%s\n", casePath.c_str());
 
 			testCase->initPrograms(progs);
 
 			for (vk::SourceCollection::Iterator progIter = progs.begin(); progIter != progs.end(); ++progIter)
 			{
-				tcu::print("    %s\n", progIter.getName().c_str());
+				try
+				{
+					const vk::ProgramIdentifier			progId		(casePath, progIter.getName());
+					const UniquePtr<vk::ProgramBinary>	binary		(vk::buildProgram(progIter.getProgram(), vk::PROGRAM_FORMAT_SPIRV));
 
-				// \todo [2015-03-20 pyry] This is POC level, next steps:
-				//  - actually build programs
-				//  - eliminate duplicates
-				//  - store as binaries + name -> prog file map
+					if (mode == BUILDMODE_BUILD)
+						writer->storeProgram(progId, *binary);
+					else
+					{
+						DE_ASSERT(mode == BUILDMODE_VERIFY);
+
+						const UniquePtr<vk::ProgramBinary>	storedBinary	(reader->loadProgram(progId));
+
+						if (binary->getSize() != storedBinary->getSize())
+							throw tcu::Exception("Binary size doesn't match");
+
+						if (deMemCmp(binary->getBinary(), storedBinary->getBinary(), binary->getSize()))
+							throw tcu::Exception("Binary contents don't match");
+					}
+
+					tcu::print("  OK: %s\n", progIter.getName().c_str());
+					stats.numSucceeded += 1;
+				}
+				catch (const std::exception& e)
+				{
+					tcu::print("  ERROR: %s: %s\n", progIter.getName().c_str(), e.what());
+					stats.numFailed += 1;
+				}
 			}
 		}
 
 		iterator.next();
 	}
+
+	return stats;
 }
 
 } // vkt
 
+namespace opt
+{
+
+DE_DECLARE_COMMAND_LINE_OPT(DstPath,	std::string);
+DE_DECLARE_COMMAND_LINE_OPT(Mode,		vkt::BuildMode);
+
+} // opt
+
+void registerOptions (de::cmdline::Parser& parser)
+{
+	using de::cmdline::Option;
+	using de::cmdline::NamedValue;
+
+	static const NamedValue<vkt::BuildMode> s_modes[] =
+	{
+		{ "build",	vkt::BUILDMODE_BUILD	},
+		{ "verify",	vkt::BUILDMODE_VERIFY	}
+	};
+
+	parser << Option<opt::DstPath>	("d", "dst-path",	"Destination path",	".")
+		   << Option<opt::Mode>		("m", "mode",		"Build mode",		s_modes,	"build");
+}
+
 int main (int argc, const char* argv[])
 {
+	de::cmdline::CommandLine	cmdLine;
+
+	{
+		de::cmdline::Parser		parser;
+		registerOptions(parser);
+		if (!parser.parse(argc, argv, &cmdLine, std::cerr))
+		{
+			parser.help(std::cout);
+			return -1;
+		}
+	}
+
 	try
 	{
-		const tcu::CommandLine	cmdLine		(argc, argv);
-		tcu::DirArchive			archive		(".");
-		tcu::TestLog			log			(cmdLine.getLogFileName(), cmdLine.getLogFlags());
+		const tcu::CommandLine	deqpCmdLine		("unused");
+		tcu::DirArchive			archive			(".");
+		tcu::TestLog			log				(deqpCmdLine.getLogFileName(), deqpCmdLine.getLogFlags());
 		tcu::Platform			platform;
-		tcu::TestContext		testCtx		(platform, archive, log, cmdLine, DE_NULL);
+		tcu::TestContext		testCtx			(platform, archive, log, deqpCmdLine, DE_NULL);
 
-		vkt::buildPrograms(testCtx);
+		const vkt::BuildStats	stats			= vkt::buildPrograms(testCtx, cmdLine.getOption<opt::DstPath>(), cmdLine.getOption<opt::Mode>());
+
+		tcu::print("DONE: %d passed, %d failed\n", stats.numSucceeded, stats.numFailed);
+
+		return stats.numFailed == 0 ? 0 : -1;
 	}
 	catch (const std::exception& e)
 	{
 		tcu::die("%s", e.what());
 	}
-
-	return 0;
 }
diff --git a/modules/vulkan/vktTestPackage.cpp b/modules/vulkan/vktTestPackage.cpp
index a06687b..a690900 100644
--- a/modules/vulkan/vktTestPackage.cpp
+++ b/modules/vulkan/vktTestPackage.cpp
@@ -26,6 +26,7 @@
 #include "tcuPlatform.hpp"
 #include "vkPlatform.hpp"
 #include "vkPrograms.hpp"
+#include "vkBinaryRegistry.hpp"
 #include "deUniquePtr.hpp"
 
 #include "vktInfo.hpp"
@@ -91,14 +92,28 @@
 	m_progCollection.clear();
 	vktCase->initPrograms(sourceProgs);
 
-	// \todo [2015-03-13 pyry] Need abstraction for this - sometimes built on run-time, sometimes loaded from archive
 	for (vk::SourceCollection::Iterator progIter = sourceProgs.begin(); progIter != sourceProgs.end(); ++progIter)
 	{
-		const std::string&				name		= progIter.getName();
-		const glu::ProgramSources&		srcProg		= progIter.getProgram();
-		de::MovePtr<vk::ProgramBinary>	binProg		= de::MovePtr<vk::ProgramBinary>(vk::buildProgram(srcProg, vk::PROGRAM_FORMAT_SPIRV));
+		const vk::ProgramIdentifier		progId		(casePath, progIter.getName());
+		de::MovePtr<vk::ProgramBinary>	binProg;
 
-		m_progCollection.add(name, binProg);
+		// \todo [2015-07-01 pyry] Command line parameter to control cache vs. build order?
+
+		try
+		{
+			binProg	= de::MovePtr<vk::ProgramBinary>(vk::buildProgram(progIter.getProgram(), vk::PROGRAM_FORMAT_SPIRV));
+		}
+		catch (const tcu::NotSupportedError&)
+		{
+			// Try to load from cache
+			const vk::BinaryRegistryReader	registry	(m_context.getTestContext().getArchive(), "vulkan/prebuilt");
+
+			binProg = de::MovePtr<vk::ProgramBinary>(registry.loadProgram(progId));
+		}
+
+		TCU_CHECK_INTERNAL(binProg);
+
+		m_progCollection.add(progId.programName, binProg);
 	}
 
 	DE_ASSERT(!m_instance);
diff --git a/scripts/build_vulkan_programs.py b/scripts/build_vulkan_programs.py
new file mode 100644
index 0000000..e3b2b11
--- /dev/null
+++ b/scripts/build_vulkan_programs.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+
+#-------------------------------------------------------------------------
+# drawElements Quality Program utilities
+# --------------------------------------
+#
+# Copyright 2015 The Android Open Source Project
+#
+# 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.
+#
+#-------------------------------------------------------------------------
+
+from build.common import *
+from build.config import *
+from build.build import *
+
+import os
+import sys
+import string
+import argparse
+import tempfile
+import shutil
+import fnmatch
+
+class Module:
+	def __init__ (self, name, dirName, binName):
+		self.name		= name
+		self.dirName	= dirName
+		self.binName	= binName
+
+VULKAN_MODULE		= Module("dEQP-VK",		"vulkan",		"deqp-vk")
+DEFAULT_BUILD_DIR	= os.path.join(tempfile.gettempdir(), "vulkan-programs", "{targetName}-{buildType}")
+DEFAULT_TARGET		= "null"
+DEFAULT_DST_DIR		= os.path.join(DEQP_DIR, "data", "vulkan", "prebuilt")
+
+def getBuildConfig (buildPathPtrn, targetName, buildType):
+	buildPath = buildPathPtrn.format(
+		targetName	= targetName,
+		buildType	= buildType)
+
+	return BuildConfig(buildPath, buildType, ["-DDEQP_TARGET=%s" % targetName])
+
+def cleanDstDir (dstPath):
+	binFiles = [f for f in os.listdir(dstPath) if os.path.isfile(os.path.join(dstPath, f)) and fnmatch.fnmatch(f, "*.spirv")]
+
+	for binFile in binFiles:
+		print "Removing %s" % os.path.join(dstPath, binFile)
+		os.remove(os.path.join(dstPath, binFile))
+
+def execBuildPrograms (buildCfg, generator, module, mode, dstPath):
+	workDir = os.path.join(buildCfg.getBuildDir(), "modules", module.dirName)
+
+	pushWorkingDir(workDir)
+
+	try:
+		binPath = generator.getBinaryPath(buildCfg.getBuildType(), os.path.join(".", "vk-build-programs"))
+		execute([binPath, "--mode", mode, "--dst-path", dstPath])
+	finally:
+		popWorkingDir()
+
+def parseArgs ():
+	parser = argparse.ArgumentParser(description = "Build SPIR-V programs",
+									 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+	parser.add_argument("-b",
+						"--build-dir",
+						dest="buildDir",
+						default=DEFAULT_BUILD_DIR,
+						help="Temporary build directory")
+	parser.add_argument("-t",
+						"--build-type",
+						dest="buildType",
+						default="Debug",
+						help="Build type")
+	parser.add_argument("-c",
+						"--deqp-target",
+						dest="targetName",
+						default=DEFAULT_TARGET,
+						help="dEQP build target")
+	parser.add_argument("--mode",
+						dest="mode",
+						default="build",
+						help="Build mode (build or verify)")
+	parser.add_argument("-d",
+						"--dst-path",
+						dest="dstPath",
+						default=DEFAULT_DST_DIR,
+						help="Destination path")
+	return parser.parse_args()
+
+if __name__ == "__main__":
+	args = parseArgs()
+
+	generator	= ANY_GENERATOR
+	buildCfg	= getBuildConfig(args.buildDir, args.targetName, args.buildType)
+	module		= VULKAN_MODULE
+
+	build(buildCfg, generator, ["vk-build-programs"])
+
+	if args.mode == "build":
+		if os.path.exists(args.dstPath):
+			cleanDstDir(args.dstPath)
+		else:
+			os.makedirs(args.dstPath)
+
+	execBuildPrograms(buildCfg, generator, module, args.mode, args.dstPath)