// Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package main // gen_interface creates the assemble/validate cpp files given the // interface.json5 file. // See README for more details. import ( "flag" "fmt" "io/ioutil" "os" "path/filepath" "sort" "strings" "github.com/flynn/json5" ) var ( outDir = flag.String("out_dir", "../../src/gpu/gl", "Where to output the GrGlAssembleInterface_* and GrGlInterface.cpp files") inTable = flag.String("in_table", "./interface.json5", "The JSON5 table to read in") dryRun = flag.Bool("dryrun", false, "Print the outputs, don't write to file") ) const ( CORE_FEATURE = "" SPACER = " " GLES_FILE_NAME = "GrGLAssembleGLESInterfaceAutogen.cpp" GL_FILE_NAME = "GrGLAssembleGLInterfaceAutogen.cpp" WEBGL_FILE_NAME = "GrGLAssembleWebGLInterfaceAutogen.cpp" INTERFACE_FILE_NAME = "GrGLInterfaceAutogen.cpp" ) // FeatureSet represents one set of requirements for each of the GL "standards" that // Skia supports. This is currently OpenGL, OpenGL ES and WebGL. // OpenGL is typically abbreviated as just "GL". // https://www.khronos.org/registry/OpenGL/index_gl.php // https://www.khronos.org/opengles/ // https://www.khronos.org/registry/webgl/specs/1.0/ type FeatureSet struct { GLReqs []Requirement `json:"GL"` GLESReqs []Requirement `json:"GLES"` WebGLReqs []Requirement `json:"WebGL"` Functions []string `json:"functions"` HardCodeFunctions []HardCodeFunction `json:"hardcode_functions"` OptionalFunctions []string `json:"optional"` // not checked in validate // only assembled/validated when testing TestOnlyFunctions []string `json:"test_functions"` Required bool `json:"required"` } // Requirement lists how we know if a function exists. Extension is the // GL extension (or the string CORE_FEATURE if it's part of the core functionality). // MinVersion optionally indicates the minimum version of a standard // that has the function. // SuffixOverride allows the extension suffix to be manually specified instead // of automatically derived from the extension name. // (for example, if an NV extension specifies some EXT extensions) type Requirement struct { Extension string `json:"ext"` // required MinVersion *GLVersion `json:"min_version"` SuffixOverride *string `json:"suffix"` } // HardCodeFunction indicates to not use the C++ macro and just directly // adds a given function ptr to the struct. type HardCodeFunction struct { PtrName string `json:"ptr_name"` CastName string `json:"cast_name"` GetName string `json:"get_name"` } var CORE_REQUIREMENT = Requirement{Extension: CORE_FEATURE, MinVersion: nil} type GLVersion [2]int // RequirementGetter functions allows us to "iterate" over the requirements // of the different standards which are stored as keys in FeatureSet and // normally not easily iterable. type RequirementGetter func(FeatureSet) []Requirement func glRequirements(fs FeatureSet) []Requirement { return fs.GLReqs } func glesRequirements(fs FeatureSet) []Requirement { return fs.GLESReqs } func webglRequirements(fs FeatureSet) []Requirement { return fs.WebGLReqs } // generateAssembleInterface creates one GrGLAssembleInterface_[type]_gen.cpp // for each of the standards func generateAssembleInterface(features []FeatureSet) { gl := fillAssembleTemplate(ASSEMBLE_INTERFACE_GL, features, glRequirements) writeToFile(*outDir, GL_FILE_NAME, gl) gles := fillAssembleTemplate(ASSEMBLE_INTERFACE_GL_ES, features, glesRequirements) writeToFile(*outDir, GLES_FILE_NAME, gles) webgl := fillAssembleTemplate(ASSEMBLE_INTERFACE_WEBGL, features, webglRequirements) writeToFile(*outDir, WEBGL_FILE_NAME, webgl) } // fillAssembleTemplate returns a generated file given a template (for a single standard) // to fill out and a slice of features with which to fill it. getReqs is used to select // the requirements for the standard we are working on. func fillAssembleTemplate(template string, features []FeatureSet, getReqs RequirementGetter) string { content := "" for _, feature := range features { // For each feature set, we are going to create a series of // if statements, which check for the requirements (e.g. extensions, version) // and inside those if branches, write the code to load the // correct function pointer to the interface. GET_PROC and // GET_PROC_SUFFIX are macros defined in C++ part of the template // to accomplish this (for a core feature and extensions, respectively). reqs := getReqs(feature) if len(reqs) == 0 { continue } // blocks holds all the if blocks generated - it will be joined with else // after and appended to content blocks := []string{} for i, req := range reqs { block := "" ifExpr := requirementIfExpression(req, true) if ifExpr != "" { if strings.HasPrefix(ifExpr, "(") { ifExpr = "if " + ifExpr + " {" } else { ifExpr = "if (" + ifExpr + ") {" } // Indent the first if statement if i == 0 { block = addLine(block, ifExpr) } else { block += ifExpr + "\n" } } // sort for determinism sort.Strings(feature.Functions) for _, function := range feature.Functions { block = assembleFunction(block, ifExpr, function, req) } sort.Strings(feature.TestOnlyFunctions) if len(feature.TestOnlyFunctions) > 0 { block += "#if GR_TEST_UTILS\n" for _, function := range feature.TestOnlyFunctions { block = assembleFunction(block, ifExpr, function, req) } block += "#endif\n" } // a hard code function does not use the C++ macro for _, hcf := range feature.HardCodeFunctions { if ifExpr != "" { // extra tab for being in an if statement block += SPACER } line := fmt.Sprintf(`functions->%s =(%s*)get(ctx, "%s");`, hcf.PtrName, hcf.CastName, hcf.GetName) block = addLine(block, line) } if ifExpr != "" { block += SPACER + "}" } blocks = append(blocks, block) } content += strings.Join(blocks, " else ") if feature.Required && reqs[0] != CORE_REQUIREMENT { content += ` else { SkASSERT(false); // Required feature return nullptr; }` } if !strings.HasSuffix(content, "\n") { content += "\n" } // Add an extra space between blocks for easier readability content += "\n" } return strings.Replace(template, "[[content]]", content, 1) } // assembleFunction is a little helper that will add a function to the interface // using an appropriate macro (e.g. if the function is added) // with an extension. func assembleFunction(block, ifExpr, function string, req Requirement) string { if ifExpr != "" { // extra tab for being in an if statement block += SPACER } suffix := deriveSuffix(req.Extension) // Some ARB extensions don't have ARB suffixes because they were written // for backwards compatibility simultaneous to adding them as required // in a new GL version. if suffix == "ARB" { suffix = "" } if req.SuffixOverride != nil { suffix = *req.SuffixOverride } if req.Extension == CORE_FEATURE || suffix == "" { block = addLine(block, fmt.Sprintf("GET_PROC(%s);", function)) } else if req.Extension != "" { block = addLine(block, fmt.Sprintf("GET_PROC_SUFFIX(%s, %s);", function, suffix)) } return block } // requirementIfExpression returns a string that is an if expression // Notably, there is no if expression if the function is a "core" function // on all supported versions. // The expressions are wrapped in parentheses so they can be safely // joined together with && or ||. func requirementIfExpression(req Requirement, isLocal bool) string { mv := req.MinVersion if req == CORE_REQUIREMENT { return "" } if req.Extension == CORE_FEATURE && mv != nil { return fmt.Sprintf("(glVer >= GR_GL_VER(%d,%d))", mv[0], mv[1]) } extVar := "fExtensions" if isLocal { extVar = "extensions" } // We know it has an extension if req.Extension != "" { if mv == nil { return fmt.Sprintf("%s.has(%q)", extVar, req.Extension) } else { return fmt.Sprintf("(glVer >= GR_GL_VER(%d,%d) && %s.has(%q))", mv[0], mv[1], extVar, req.Extension) } } abort("ERROR: requirement must have ext") return "ERROR" } // driveSuffix returns the suffix of the function associated with the given // extension. func deriveSuffix(ext string) string { // Some extensions begin with GL_ and then have the actual // extension like KHR, EXT etc. ext = strings.TrimPrefix(ext, "GL_") return strings.Split(ext, "_")[0] } // addLine is a little helper function which handles the newline and tab func addLine(str, line string) string { return str + SPACER + line + "\n" } func writeToFile(parent, file, content string) { p := filepath.Join(parent, file) if *dryRun { fmt.Printf("Writing to %s\n", p) fmt.Println(content) } else { if err := ioutil.WriteFile(p, []byte(content), 0644); err != nil { abort("Error while writing to file %s: %s", p, err) } } } // validationEntry is a helper struct that contains anything // necessary to make validation code for a given standard. type validationEntry struct { StandardCheck string GetReqs RequirementGetter } func generateValidateInterface(features []FeatureSet) { standards := []validationEntry{ { StandardCheck: "GR_IS_GR_GL(fStandard)", GetReqs: glRequirements, }, { StandardCheck: "GR_IS_GR_GL_ES(fStandard)", GetReqs: glesRequirements, }, { StandardCheck: "GR_IS_GR_WEBGL(fStandard)", GetReqs: webglRequirements, }, } content := "" // For each feature, we are going to generate a series of // boolean expressions which check that the functions we thought // were gathered during the assemble phase actually were applied to // the interface (functionCheck). This check will be guarded // another set of if statements (one per standard) based // on the same requirements (standardChecks) that were used when // assembling the interface. for _, feature := range features { if allReqsAreCore(feature) { content += functionCheck(feature, 1) } else { content += SPACER standardChecks := []string{} for _, std := range standards { reqs := std.GetReqs(feature) if reqs == nil || len(reqs) == 0 { continue } expr := []string{} for _, r := range reqs { e := requirementIfExpression(r, false) if e != "" { expr = append(expr, e) } } check := "" if len(expr) == 0 { check = fmt.Sprintf("%s", std.StandardCheck) } else { lineBreak := "\n" + SPACER + " " check = fmt.Sprintf("(%s && (%s%s))", std.StandardCheck, lineBreak, strings.Join(expr, " ||"+lineBreak)) } standardChecks = append(standardChecks, check) } content += fmt.Sprintf("if (%s) {\n", strings.Join(standardChecks, " ||\n"+SPACER+" ")) content += functionCheck(feature, 2) content += SPACER + "}\n" } // add additional line between each block content += "\n" } content = strings.Replace(VALIDATE_INTERFACE, "[[content]]", content, 1) writeToFile(*outDir, INTERFACE_FILE_NAME, content) } // functionCheck returns an if statement that checks that all functions // in the passed in slice are on the interface (that is, they are truthy // on the fFunctions struct) func functionCheck(feature FeatureSet, indentLevel int) string { // sort for determinism sort.Strings(feature.Functions) indent := strings.Repeat(SPACER, indentLevel) checks := []string{} for _, function := range feature.Functions { if in(function, feature.OptionalFunctions) { continue } checks = append(checks, "!fFunctions.f"+function) } testOnly := []string{} for _, function := range feature.TestOnlyFunctions { if in(function, feature.OptionalFunctions) { continue } testOnly = append(testOnly, "!fFunctions.f"+function) } for _, hcf := range feature.HardCodeFunctions { checks = append(checks, "!fFunctions."+hcf.PtrName) } preCheck := "" if len(testOnly) != 0 { preCheck = fmt.Sprintf(`#if GR_TEST_UTILS %sif (%s) { %s%sRETURN_FALSE_INTERFACE; %s} #endif `, indent, strings.Join(testOnly, " ||\n"+indent+" "), indent, SPACER, indent) } if len(checks) == 0 { return preCheck + strings.Repeat(SPACER, indentLevel) + "// all functions were marked optional or test_only\n" } return preCheck + fmt.Sprintf(`%sif (%s) { %s%sRETURN_FALSE_INTERFACE; %s} `, indent, strings.Join(checks, " ||\n"+indent+" "), indent, SPACER, indent) } // allReqsAreCore returns true iff the FeatureSet is part of "core" for // all standards func allReqsAreCore(feature FeatureSet) bool { if feature.GLReqs == nil || feature.GLESReqs == nil { return false } return feature.GLReqs[0] == CORE_REQUIREMENT && feature.GLESReqs[0] == CORE_REQUIREMENT && feature.WebGLReqs[0] == CORE_REQUIREMENT } func validateFeatures(features []FeatureSet) { seen := map[string]bool{} for _, feature := range features { for _, fn := range feature.Functions { if seen[fn] { abort("ERROR: Duplicate function %s", fn) } seen[fn] = true } for _, fn := range feature.TestOnlyFunctions { if seen[fn] { abort("ERROR: Duplicate function %s\n", fn) } seen[fn] = true } } } // in returns true if |s| is *in* |a| slice. func in(s string, a []string) bool { for _, x := range a { if x == s { return true } } return false } func abort(fmtStr string, inputs ...interface{}) { fmt.Printf(fmtStr+"\n", inputs...) os.Exit(1) } func main() { flag.Parse() b, err := ioutil.ReadFile(*inTable) if err != nil { abort("Could not read file %s", err) } dir, err := os.Open(*outDir) if err != nil { abort("Could not write to output dir %s", err) } defer dir.Close() if fi, err := dir.Stat(); err != nil { abort("Error getting info about %s: %s", *outDir, err) } else if !fi.IsDir() { abort("%s must be a directory", *outDir) } features := []FeatureSet{} err = json5.Unmarshal(b, &features) if err != nil { abort("Invalid JSON: %s", err) } validateFeatures(features) generateAssembleInterface(features) generateValidateInterface(features) }