/* * Copyright (C) 2014 Patrick Mours * SPDX-License-Identifier: BSD-3-Clause */ #include "effect_lexer.hpp" #include "effect_parser.hpp" #include "effect_codegen.hpp" #include // std::toupper #include #include #include #include struct on_scope_exit { template explicit on_scope_exit(F lambda) : leave(lambda) { } ~on_scope_exit() { leave(); } std::function leave; }; bool reshadefx::parser::parse(std::string input, codegen *backend) { _lexer.reset(new lexer(std::move(input))); // Set backend for subsequent code-generation _codegen = backend; assert(backend != nullptr); consume(); bool parse_success = true; bool current_success = true; while (!peek(tokenid::end_of_file)) { parse_top(current_success); if (!current_success) parse_success = false; } return parse_success; } void reshadefx::parser::parse_top(bool &parse_success) { if (accept(tokenid::namespace_)) { // Anonymous namespaces are not supported right now, so an identifier is a must if (!expect(tokenid::identifier)) { parse_success = false; return; } const auto name = std::move(_token.literal_as_string); if (!expect('{')) { parse_success = false; return; } enter_namespace(name); bool current_success = true; bool parse_success_namespace = true; // Recursively parse top level statements until the namespace is closed again while (!peek('}')) // Empty namespaces are valid { parse_top(current_success); if (!current_success) parse_success_namespace = false; } leave_namespace(); parse_success = expect('}') && parse_success_namespace; } else if (accept(tokenid::struct_)) // Structure keyword found, parse the structure definition { // Structure definitions are terminated with a semicolon parse_success = parse_struct() && expect(';'); } else if (accept(tokenid::technique)) // Technique keyword found, parse the technique definition { parse_success = parse_technique(); } else { if (type type; parse_type(type)) // Type found, this can be either a variable or a function declaration { parse_success = expect(tokenid::identifier); if (!parse_success) return; if (peek('(')) { const auto name = std::move(_token.literal_as_string); // This is definitely a function declaration, so parse it if (!parse_function(type, name)) { // Insert dummy function into symbol table, so later references can be resolved despite the error insert_symbol(name, { symbol_type::function, ~0u, { type::t_function } }, true); parse_success = false; return; } } else { // There may be multiple variable names after the type, handle them all unsigned int count = 0; do { if (count++ > 0 && !(expect(',') && expect(tokenid::identifier))) { parse_success = false; return; } const auto name = std::move(_token.literal_as_string); if (!parse_variable(type, name, true)) { // Insert dummy variable into symbol table, so later references can be resolved despite the error insert_symbol(name, { symbol_type::variable, ~0u, type }, true); // Skip the rest of the statement consume_until(';'); parse_success = false; return; } } while (!peek(';')); // Variable declarations are terminated with a semicolon parse_success = expect(';'); } } else if (accept(';')) // Ignore single semicolons in the source { parse_success = true; } else { // Unexpected token in source stream, consume and report an error about it consume(); // Only add another error message if succeeded parsing previously // This is done to avoid walls of error messages because of consequential errors following a top-level syntax mistake if (parse_success) error(_token.location, 3000, "syntax error: unexpected '" + token::id_to_name(_token.id) + '\''); parse_success = false; } } } bool reshadefx::parser::parse_statement(bool scoped) { if (!_codegen->is_in_block()) return error(_token_next.location, 0, "unreachable code"), false; unsigned int loop_control = 0; unsigned int selection_control = 0; // Read any loop and branch control attributes first while (accept('[')) { enum control_mask { unroll = 0x1, dont_unroll = 0x2, flatten = (0x1 << 4), dont_flatten = (0x2 << 4), switch_force_case = (0x4 << 4), switch_call = (0x8 << 4) }; const auto attribute = std::move(_token_next.literal_as_string); if (!expect(tokenid::identifier) || !expect(']')) return false; if (attribute == "unroll") loop_control |= unroll; else if (attribute == "loop" || attribute == "fastopt") loop_control |= dont_unroll; else if (attribute == "flatten") selection_control |= flatten; else if (attribute == "branch") selection_control |= dont_flatten; else if (attribute == "forcecase") selection_control |= switch_force_case; else if (attribute == "call") selection_control |= switch_call; else warning(_token.location, 0, "unknown attribute"); if ((loop_control & (unroll | dont_unroll)) == (unroll | dont_unroll)) return error(_token.location, 3524, "can't use loop and unroll attributes together"), false; if ((selection_control & (flatten | dont_flatten)) == (flatten | dont_flatten)) return error(_token.location, 3524, "can't use branch and flatten attributes together"), false; } // Shift by two so that the possible values are 0x01 for 'flatten' and 0x02 for 'dont_flatten', equivalent to 'unroll' and 'dont_unroll' selection_control >>= 4; if (peek('{')) // Parse statement block return parse_statement_block(scoped); else if (accept(';')) // Ignore empty statements return true; // Most statements with the exception of declarations are only valid inside functions if (_codegen->is_in_function()) { assert(_current_function != nullptr); const auto location = _token_next.location; if (accept(tokenid::if_)) { codegen::id true_block = _codegen->create_block(); // Block which contains the statements executed when the condition is true codegen::id false_block = _codegen->create_block(); // Block which contains the statements executed when the condition is false const codegen::id merge_block = _codegen->create_block(); // Block that is executed after the branch re-merged with the current control flow expression condition; if (!expect('(') || !parse_expression(condition) || !expect(')')) return false; else if (!condition.type.is_scalar()) return error(condition.location, 3019, "if statement conditional expressions must evaluate to a scalar"), false; // Load condition and convert to boolean value as required by 'OpBranchConditional' condition.add_cast_operation({ type::t_bool, 1, 1 }); const codegen::id condition_value = _codegen->emit_load(condition); const codegen::id condition_block = _codegen->leave_block_and_branch_conditional(condition_value, true_block, false_block); { // Then block of the if statement _codegen->enter_block(true_block); if (!parse_statement(true)) return false; true_block = _codegen->leave_block_and_branch(merge_block); } { // Else block of the if statement _codegen->enter_block(false_block); if (accept(tokenid::else_) && !parse_statement(true)) return false; false_block = _codegen->leave_block_and_branch(merge_block); } _codegen->enter_block(merge_block); // Emit structured control flow for an if statement and connect all basic blocks _codegen->emit_if(location, condition_value, condition_block, true_block, false_block, selection_control); return true; } if (accept(tokenid::switch_)) { const codegen::id merge_block = _codegen->create_block(); // Block that is executed after the switch re-merged with the current control flow expression selector_exp; if (!expect('(') || !parse_expression(selector_exp) || !expect(')')) return false; else if (!selector_exp.type.is_scalar()) return error(selector_exp.location, 3019, "switch statement expression must evaluate to a scalar"), false; // Load selector and convert to integral value as required by switch instruction selector_exp.add_cast_operation({ type::t_int, 1, 1 }); const auto selector_value = _codegen->emit_load(selector_exp); const auto selector_block = _codegen->leave_block_and_switch(selector_value, merge_block); if (!expect('{')) return false; _loop_break_target_stack.push_back(merge_block); on_scope_exit _([this]() { _loop_break_target_stack.pop_back(); }); bool parse_success = true; // The default case jumps to the end of the switch statement if not overwritten codegen::id default_label = merge_block, default_block = merge_block; codegen::id current_label = _codegen->create_block(); std::vector case_literal_and_labels, case_blocks; size_t last_case_label_index = 0; // Enter first switch statement body block _codegen->enter_block(current_label); while (!peek(tokenid::end_of_file)) { while (accept(tokenid::case_) || accept(tokenid::default_)) { if (_token.id == tokenid::case_) { expression case_label; if (!parse_expression(case_label)) return consume_until('}'), false; else if (!case_label.type.is_scalar() || !case_label.type.is_integral() || !case_label.is_constant) return error(case_label.location, 3020, "invalid type for case expression - value must be an integer scalar"), consume_until('}'), false; // Check for duplicate case values for (size_t i = 0; i < case_literal_and_labels.size(); i += 2) { if (case_literal_and_labels[i] == case_label.constant.as_uint[0]) { parse_success = false; error(case_label.location, 3532, "duplicate case " + std::to_string(case_label.constant.as_uint[0])); break; } } case_blocks.emplace_back(); // This is set to the actual block below case_literal_and_labels.push_back(case_label.constant.as_uint[0]); case_literal_and_labels.push_back(current_label); } else { // Check if the default label was already changed by a previous 'default' statement if (default_label != merge_block) { parse_success = false; error(_token.location, 3532, "duplicate default in switch statement"); } default_label = current_label; default_block = 0; // This is set to the actual block below } if (!expect(':')) return consume_until('}'), false; } // It is valid for no statement to follow if this is the last label in the switch body const bool end_of_switch = peek('}'); if (!end_of_switch && !parse_statement(true)) return consume_until('}'), false; // Handle fall-through case and end of switch statement if (peek(tokenid::case_) || peek(tokenid::default_) || end_of_switch) { if (_codegen->is_in_block()) // Disallow fall-through for now { parse_success = false; error(_token_next.location, 3533, "non-empty case statements must have break or return"); } const codegen::id next_label = end_of_switch ? merge_block : _codegen->create_block(); // This is different from 'current_label', since there may have been branching logic inside the case, which would have changed the active block const codegen::id current_block = _codegen->leave_block_and_branch(next_label); if (0 == default_block) default_block = current_block; for (size_t i = last_case_label_index; i < case_blocks.size(); ++i) // Need to use the initial label for the switch table, but the current block to merge all the block data case_blocks[i] = current_block; current_label = next_label; _codegen->enter_block(current_label); if (end_of_switch) // We reached the end, nothing more to do break; last_case_label_index = case_blocks.size(); } } if (case_literal_and_labels.empty() && default_label == merge_block) warning(location, 5002, "switch statement contains no 'case' or 'default' labels"); // Emit structured control flow for a switch statement and connect all basic blocks _codegen->emit_switch(location, selector_value, selector_block, default_label, default_block, case_literal_and_labels, case_blocks, selection_control); return expect('}') && parse_success; } if (accept(tokenid::for_)) { if (!expect('(')) return false; enter_scope(); on_scope_exit _([this]() { leave_scope(); }); // Parse initializer first if (type type; parse_type(type)) { unsigned int count = 0; do { // There may be multiple declarations behind a type, so loop through them if (count++ > 0 && !expect(',')) return false; if (!expect(tokenid::identifier) || !parse_variable(type, std::move(_token.literal_as_string))) return false; } while (!peek(';')); } else { // Initializer can also contain an expression if not a variable declaration list and not empty if (!peek(';')) { expression expression; if (!parse_expression(expression)) return false; } } if (!expect(';')) return false; const codegen::id merge_block = _codegen->create_block(); // Block that is executed after the loop const codegen::id header_label = _codegen->create_block(); // Pointer to the loop merge instruction const codegen::id continue_label = _codegen->create_block(); // Pointer to the continue block codegen::id loop_block = _codegen->create_block(); // Pointer to the main loop body block codegen::id condition_block = _codegen->create_block(); // Pointer to the condition check codegen::id condition_value = 0; // End current block by branching to the next label const codegen::id prev_block = _codegen->leave_block_and_branch(header_label); { // Begin loop block (this header is used for explicit structured control flow) _codegen->enter_block(header_label); _codegen->leave_block_and_branch(condition_block); } { // Parse condition block _codegen->enter_block(condition_block); if (!peek(';')) { expression condition; if (!parse_expression(condition)) return false; if (!condition.type.is_scalar()) return error(condition.location, 3019, "scalar value expected"), false; // Evaluate condition and branch to the right target condition.add_cast_operation({ type::t_bool, 1, 1 }); condition_value = _codegen->emit_load(condition); condition_block = _codegen->leave_block_and_branch_conditional(condition_value, loop_block, merge_block); } else // It is valid for there to be no condition expression { condition_block = _codegen->leave_block_and_branch(loop_block); } if (!expect(';')) return false; } { // Parse loop continue block into separate block so it can be appended to the end down the line _codegen->enter_block(continue_label); if (!peek(')')) { expression continue_exp; if (!parse_expression(continue_exp)) return false; } if (!expect(')')) return false; // Branch back to the loop header at the end of the continue block _codegen->leave_block_and_branch(header_label); } { // Parse loop body block _codegen->enter_block(loop_block); _loop_break_target_stack.push_back(merge_block); _loop_continue_target_stack.push_back(continue_label); const bool parse_success = parse_statement(false); _loop_break_target_stack.pop_back(); _loop_continue_target_stack.pop_back(); if (!parse_success) return false; loop_block = _codegen->leave_block_and_branch(continue_label); } // Add merge block label to the end of the loop _codegen->enter_block(merge_block); // Emit structured control flow for a loop statement and connect all basic blocks _codegen->emit_loop(location, condition_value, prev_block, header_label, condition_block, loop_block, continue_label, loop_control); return true; } if (accept(tokenid::while_)) { enter_scope(); on_scope_exit _([this]() { leave_scope(); }); const codegen::id merge_block = _codegen->create_block(); const codegen::id header_label = _codegen->create_block(); const codegen::id continue_label = _codegen->create_block(); codegen::id loop_block = _codegen->create_block(); codegen::id condition_block = _codegen->create_block(); codegen::id condition_value = 0; // End current block by branching to the next label const codegen::id prev_block = _codegen->leave_block_and_branch(header_label); { // Begin loop block _codegen->enter_block(header_label); _codegen->leave_block_and_branch(condition_block); } { // Parse condition block _codegen->enter_block(condition_block); expression condition; if (!expect('(') || !parse_expression(condition) || !expect(')')) return false; else if (!condition.type.is_scalar()) return error(condition.location, 3019, "scalar value expected"), false; // Evaluate condition and branch to the right target condition.add_cast_operation({ type::t_bool, 1, 1 }); condition_value = _codegen->emit_load(condition); condition_block = _codegen->leave_block_and_branch_conditional(condition_value, loop_block, merge_block); } { // Parse loop body block _codegen->enter_block(loop_block); _loop_break_target_stack.push_back(merge_block); _loop_continue_target_stack.push_back(continue_label); const bool parse_success = parse_statement(false); _loop_break_target_stack.pop_back(); _loop_continue_target_stack.pop_back(); if (!parse_success) return false; loop_block = _codegen->leave_block_and_branch(continue_label); } { // Branch back to the loop header in empty continue block _codegen->enter_block(continue_label); _codegen->leave_block_and_branch(header_label); } // Add merge block label to the end of the loop _codegen->enter_block(merge_block); // Emit structured control flow for a loop statement and connect all basic blocks _codegen->emit_loop(location, condition_value, prev_block, header_label, condition_block, loop_block, continue_label, loop_control); return true; } if (accept(tokenid::do_)) { const codegen::id merge_block = _codegen->create_block(); const codegen::id header_label = _codegen->create_block(); const codegen::id continue_label = _codegen->create_block(); codegen::id loop_block = _codegen->create_block(); codegen::id condition_value = 0; // End current block by branching to the next label const codegen::id prev_block = _codegen->leave_block_and_branch(header_label); { // Begin loop block _codegen->enter_block(header_label); _codegen->leave_block_and_branch(loop_block); } { // Parse loop body block _codegen->enter_block(loop_block); _loop_break_target_stack.push_back(merge_block); _loop_continue_target_stack.push_back(continue_label); const bool parse_success = parse_statement(true); _loop_break_target_stack.pop_back(); _loop_continue_target_stack.pop_back(); if (!parse_success) return false; loop_block = _codegen->leave_block_and_branch(continue_label); } { // Continue block does the condition evaluation _codegen->enter_block(continue_label); expression condition; if (!expect(tokenid::while_) || !expect('(') || !parse_expression(condition) || !expect(')') || !expect(';')) return false; else if (!condition.type.is_scalar()) return error(condition.location, 3019, "scalar value expected"), false; // Evaluate condition and branch to the right target condition.add_cast_operation({ type::t_bool, 1, 1 }); condition_value = _codegen->emit_load(condition); _codegen->leave_block_and_branch_conditional(condition_value, header_label, merge_block); } // Add merge block label to the end of the loop _codegen->enter_block(merge_block); // Emit structured control flow for a loop statement and connect all basic blocks _codegen->emit_loop(location, condition_value, prev_block, header_label, 0, loop_block, continue_label, loop_control); return true; } if (accept(tokenid::break_)) { if (_loop_break_target_stack.empty()) return error(location, 3518, "break must be inside loop"), false; // Branch to the break target of the inner most loop on the stack _codegen->leave_block_and_branch(_loop_break_target_stack.back(), 1); return expect(';'); } if (accept(tokenid::continue_)) { if (_loop_continue_target_stack.empty()) return error(location, 3519, "continue must be inside loop"), false; // Branch to the continue target of the inner most loop on the stack _codegen->leave_block_and_branch(_loop_continue_target_stack.back(), 2); return expect(';'); } if (accept(tokenid::return_)) { const type &ret_type = _current_function->return_type; if (!peek(';')) { expression expression; if (!parse_expression(expression)) return consume_until(';'), false; // Cannot return to void if (ret_type.is_void()) // Consume the semicolon that follows the return expression so that parsing may continue return error(location, 3079, "void functions cannot return a value"), accept(';'), false; // Cannot return arrays from a function if (expression.type.is_array() || !type::rank(expression.type, ret_type)) return error(location, 3017, "expression (" + expression.type.description() + ") does not match function return type (" + ret_type.description() + ')'), accept(';'), false; // Load return value and perform implicit cast to function return type if (expression.type.components() > ret_type.components()) warning(expression.location, 3206, "implicit truncation of vector type"); expression.add_cast_operation(ret_type); const auto return_value = _codegen->emit_load(expression); _codegen->leave_block_and_return(return_value); } else if (!ret_type.is_void()) { // No return value was found, but the function expects one error(location, 3080, "function must return a value"); // Consume the semicolon that follows the return expression so that parsing may continue accept(';'); return false; } else { _codegen->leave_block_and_return(); } return expect(';'); } if (accept(tokenid::discard_)) { // Leave the current function block _codegen->leave_block_and_kill(); return expect(';'); } } // Handle variable declarations if (type type; parse_type(type)) { unsigned int count = 0; do { // There may be multiple declarations behind a type, so loop through them if (count++ > 0 && !expect(',')) // Try to consume the rest of the declaration so that parsing may continue despite the error return consume_until(';'), false; if (!expect(tokenid::identifier) || !parse_variable(type, std::move(_token.literal_as_string))) return consume_until(';'), false; } while (!peek(';')); return expect(';'); } // Handle expression statements if (expression expression; parse_expression(expression)) return expect(';'); // A statement has to be terminated with a semicolon // Gracefully consume any remaining characters until the statement would usually end, so that parsing may continue despite the error consume_until(';'); return false; } bool reshadefx::parser::parse_statement_block(bool scoped) { if (!expect('{')) return false; if (scoped) enter_scope(); // Parse statements until the end of the block is reached while (!peek('}') && !peek(tokenid::end_of_file)) { if (!parse_statement(true)) { if (scoped) leave_scope(); // Ignore the rest of this block unsigned int level = 0; while (!peek(tokenid::end_of_file)) { if (accept('{')) { ++level; } else if (accept('}')) { if (level-- == 0) break; } // These braces are necessary to match the 'else' to the correct 'if' statement else { consume(); } } return false; } } if (scoped) leave_scope(); return expect('}'); } bool reshadefx::parser::parse_type(type &type) { type.qualifiers = 0; accept_type_qualifiers(type); if (!accept_type_class(type)) return false; if (type.is_integral() && (type.has(type::q_centroid) || type.has(type::q_noperspective))) return error(_token.location, 4576, "signature specifies invalid interpolation mode for integer component type"), false; else if (type.has(type::q_centroid) && !type.has(type::q_noperspective)) type.qualifiers |= type::q_linear; return true; } bool reshadefx::parser::parse_array_size(type &type) { // Reset array length to zero before checking if one exists type.array_length = 0; if (accept('[')) { if (accept(']')) { // No length expression, so this is an unsized array type.array_length = -1; } else if (expression expression; parse_expression(expression) && expect(']')) { if (!expression.is_constant || !(expression.type.is_scalar() && expression.type.is_integral())) return error(expression.location, 3058, "array dimensions must be literal scalar expressions"), false; type.array_length = expression.constant.as_uint[0]; if (type.array_length < 1 || type.array_length > 65536) return error(expression.location, 3059, "array dimension must be between 1 and 65536"), false; } else { return false; } } // Multi-dimensional arrays are not supported if (peek('[')) return error(_token_next.location, 3119, "arrays cannot be multi-dimensional"), false; return true; } bool reshadefx::parser::parse_annotations(std::vector &annotations) { // Check if annotations exist and return early if none do if (!accept('<')) return true; bool parse_success = true; while (!peek('>')) { if (type type; accept_type_class(type)) warning(_token.location, 4717, "type prefixes for annotations are deprecated and ignored"); if (!expect(tokenid::identifier)) return consume_until('>'), false; auto name = std::move(_token.literal_as_string); if (expression expression; !expect('=') || !parse_expression_multary(expression) || !expect(';')) return consume_until('>'), false; else if (expression.is_constant) annotations.push_back({ expression.type, std::move(name), std::move(expression.constant) }); else // Continue parsing annotations despite this not being a constant, since the syntax is still correct parse_success = false, error(expression.location, 3011, "value must be a literal expression"); } return expect('>') && parse_success; } bool reshadefx::parser::parse_struct() { const auto location = std::move(_token.location); struct_info info; // The structure name is optional if (accept(tokenid::identifier)) info.name = std::move(_token.literal_as_string); else info.name = "_anonymous_struct_" + std::to_string(location.line) + '_' + std::to_string(location.column); info.unique_name = 'S' + current_scope().name + info.name; std::replace(info.unique_name.begin(), info.unique_name.end(), ':', '_'); if (!expect('{')) return false; bool parse_success = true; while (!peek('}')) // Empty structures are possible { struct_member_info member; if (!parse_type(member.type)) return error(_token_next.location, 3000, "syntax error: unexpected '" + token::id_to_name(_token_next.id) + "', expected struct member type"), consume_until('}'), accept(';'), false; unsigned int count = 0; do { if (count++ > 0 && !expect(',')) return consume_until('}'), accept(';'), false; if (!expect(tokenid::identifier)) return consume_until('}'), accept(';'), false; member.name = std::move(_token.literal_as_string); member.location = std::move(_token.location); if (member.type.is_void()) parse_success = false, error(member.location, 3038, '\'' + member.name + "': struct members cannot be void"); if (member.type.is_struct()) // Nesting structures would make input/output argument flattening more complicated, so prevent it for now parse_success = false, error(member.location, 3090, '\'' + member.name + "': nested struct members are not supported"); if (member.type.has(type::q_in) || member.type.has(type::q_out)) parse_success = false, error(member.location, 3055, '\'' + member.name + "': struct members cannot be declared 'in' or 'out'"); if (member.type.has(type::q_const)) parse_success = false, error(member.location, 3035, '\'' + member.name + "': struct members cannot be declared 'const'"); if (member.type.has(type::q_extern)) parse_success = false, error(member.location, 3006, '\'' + member.name + "': struct members cannot be declared 'extern'"); if (member.type.has(type::q_static)) parse_success = false, error(member.location, 3007, '\'' + member.name + "': struct members cannot be declared 'static'"); if (member.type.has(type::q_uniform)) parse_success = false, error(member.location, 3047, '\'' + member.name + "': struct members cannot be declared 'uniform'"); if (member.type.has(type::q_groupshared)) parse_success = false, error(member.location, 3010, '\'' + member.name + "': struct members cannot be declared 'groupshared'"); // Modify member specific type, so that following members in the declaration list are not affected by this if (!parse_array_size(member.type)) return consume_until('}'), accept(';'), false; else if (member.type.array_length < 0) parse_success = false, error(member.location, 3072, '\'' + member.name + "': array dimensions of struct members must be explicit"); // Structure members may have semantics to use them as input/output types if (accept(':')) { if (!expect(tokenid::identifier)) return consume_until('}'), accept(';'), false; member.semantic = std::move(_token.literal_as_string); // Make semantic upper case to simplify comparison later on std::transform(member.semantic.begin(), member.semantic.end(), member.semantic.begin(), [](std::string::value_type c) { return static_cast(std::toupper(c)); }); if (member.semantic.compare(0, 3, "SV_") != 0) { // Always numerate semantics, so that e.g. TEXCOORD and TEXCOORD0 point to the same location if (const char c = member.semantic.back(); c < '0' || c > '9') member.semantic += '0'; if (member.type.is_integral() && !member.type.has(type::q_nointerpolation)) { member.type.qualifiers |= type::q_nointerpolation; // Integer fields do not interpolate, so make this explicit (to avoid issues with GLSL) warning(member.location, 4568, '\'' + member.name + "': integer fields have the 'nointerpolation' qualifier by default"); } } else { // Remove optional trailing zero from system value semantics, so that e.g. SV_POSITION and SV_POSITION0 mean the same thing if (member.semantic.back() == '0' && (member.semantic[member.semantic.size() - 2] < '0' || member.semantic[member.semantic.size() - 2] > '9')) member.semantic.pop_back(); } } // Save member name and type for book keeping info.member_list.push_back(member); } while (!peek(';')); if (!expect(';')) return consume_until('}'), accept(';'), false; } // Empty structures are valid, but not usually intended, so emit a warning if (info.member_list.empty()) warning(location, 5001, "struct has no members"); // Define the structure now that information about all the member types was gathered const auto id = _codegen->define_struct(location, info); // Insert the symbol into the symbol table const symbol symbol = { symbol_type::structure, id }; if (!insert_symbol(info.name, symbol, true)) return error(location, 3003, "redefinition of '" + info.name + '\''), false; return expect('}') && parse_success; } bool reshadefx::parser::parse_function(type type, std::string name) { const auto location = std::move(_token.location); if (!expect('(')) // Functions always have a parameter list return false; if (type.qualifiers != 0) return error(location, 3047, '\'' + name + "': function return type cannot have any qualifiers"), false; function_info info; info.name = name; info.unique_name = 'F' + current_scope().name + name; std::replace(info.unique_name.begin(), info.unique_name.end(), ':', '_'); info.return_type = type; _current_function = &info; bool parse_success = true; bool expect_parenthesis = true; // Enter function scope (and leave it again when finished parsing this function) enter_scope(); on_scope_exit _([this]() { leave_scope(); _codegen->leave_function(); _current_function = nullptr; }); while (!peek(')')) { if (!info.parameter_list.empty() && !expect(',')) { parse_success = false; expect_parenthesis = false; consume_until(')'); break; } struct_member_info param; if (!parse_type(param.type)) { error(_token_next.location, 3000, "syntax error: unexpected '" + token::id_to_name(_token_next.id) + "', expected parameter type"); parse_success = false; expect_parenthesis = false; consume_until(')'); break; } if (!expect(tokenid::identifier)) { parse_success = false; expect_parenthesis = false; consume_until(')'); break; } param.name = std::move(_token.literal_as_string); param.location = std::move(_token.location); if (param.type.is_void()) parse_success = false, error(param.location, 3038, '\'' + param.name + "': function parameters cannot be void"); if (param.type.has(type::q_extern)) parse_success = false, error(param.location, 3006, '\'' + param.name + "': function parameters cannot be declared 'extern'"); if (param.type.has(type::q_static)) parse_success = false, error(param.location, 3007, '\'' + param.name + "': function parameters cannot be declared 'static'"); if (param.type.has(type::q_uniform)) parse_success = false, error(param.location, 3047, '\'' + param.name + "': function parameters cannot be declared 'uniform', consider placing in global scope instead"); if (param.type.has(type::q_groupshared)) parse_success = false, error(param.location, 3010, '\'' + param.name + "': function parameters cannot be declared 'groupshared'"); if (param.type.has(type::q_out) && param.type.has(type::q_const)) parse_success = false, error(param.location, 3046, '\'' + param.name + "': output parameters cannot be declared 'const'"); else if (!param.type.has(type::q_out)) param.type.qualifiers |= type::q_in; // Function parameters are implicitly 'in' if not explicitly defined as 'out' if (!parse_array_size(param.type)) { parse_success = false; expect_parenthesis = false; consume_until(')'); break; } else if (param.type.array_length < 0) { parse_success = false; error(param.location, 3072, '\'' + param.name + "': array dimensions of function parameters must be explicit"); } // Handle parameter type semantic if (accept(':')) { if (!expect(tokenid::identifier)) { parse_success = false; expect_parenthesis = false; consume_until(')'); break; } param.semantic = std::move(_token.literal_as_string); // Make semantic upper case to simplify comparison later on std::transform(param.semantic.begin(), param.semantic.end(), param.semantic.begin(), [](std::string::value_type c) { return static_cast(std::toupper(c)); }); if (param.semantic.compare(0, 3, "SV_") != 0) { // Always numerate semantics, so that e.g. TEXCOORD and TEXCOORD0 point to the same location if (const char c = param.semantic.back(); c < '0' || c > '9') param.semantic += '0'; if (param.type.is_integral() && !param.type.has(type::q_nointerpolation)) { param.type.qualifiers |= type::q_nointerpolation; // Integer parameters do not interpolate, so make this explicit (to avoid issues with GLSL) warning(param.location, 4568, '\'' + param.name + "': integer parameters have the 'nointerpolation' qualifier by default"); } } else { // Remove optional trailing zero from system value semantics, so that e.g. SV_POSITION and SV_POSITION0 mean the same thing if (param.semantic.back() == '0' && (param.semantic[param.semantic.size() - 2] < '0' || param.semantic[param.semantic.size() - 2] > '9')) param.semantic.pop_back(); } } info.parameter_list.push_back(std::move(param)); } if (expect_parenthesis && !expect(')')) return false; // Handle return type semantic if (accept(':')) { if (!expect(tokenid::identifier)) return false; if (type.is_void()) return error(_token.location, 3076, '\'' + name + "': void function cannot have a semantic"), false; info.return_semantic = std::move(_token.literal_as_string); // Make semantic upper case to simplify comparison later on std::transform(info.return_semantic.begin(), info.return_semantic.end(), info.return_semantic.begin(), [](std::string::value_type c) { return static_cast(std::toupper(c)); }); } // Check if this is a function declaration without a body if (accept(';')) return error(location, 3510, '\'' + name + "': function is missing an implementation"), false; // Define the function now that information about the declaration was gathered const auto id = _codegen->define_function(location, info); // Insert the function and parameter symbols into the symbol table and update current function pointer to the permanent one symbol symbol = { symbol_type::function, id, { type::t_function } }; symbol.function = _current_function = &_codegen->get_function(id); if (!insert_symbol(name, symbol, true)) return error(location, 3003, "redefinition of '" + name + '\''), false; for (const struct_member_info ¶m : info.parameter_list) if (!insert_symbol(param.name, { symbol_type::variable, param.definition, param.type })) return error(param.location, 3003, "redefinition of '" + param.name + '\''), false; // A function has to start with a new block _codegen->enter_block(_codegen->create_block()); if (!parse_statement_block(false)) parse_success = false; // Add implicit return statement to the end of functions if (_codegen->is_in_block()) _codegen->leave_block_and_return(); return parse_success; } bool reshadefx::parser::parse_variable(type type, std::string name, bool global) { const auto location = std::move(_token.location); if (type.is_void()) return error(location, 3038, '\'' + name + "': variables cannot be void"), false; if (type.has(type::q_in) || type.has(type::q_out)) return error(location, 3055, '\'' + name + "': variables cannot be declared 'in' or 'out'"), false; // Local and global variables have different requirements if (global) { // Check that type qualifier combinations are valid if (type.has(type::q_static)) { // Global variables that are 'static' cannot be of another storage class if (type.has(type::q_uniform)) return error(location, 3007, '\'' + name + "': uniform global variables cannot be declared 'static'"), false; // The 'volatile' qualifier is only valid memory object declarations that are storage images or uniform blocks if (type.has(type::q_volatile)) return error(location, 3008, '\'' + name + "': global variables cannot be declared 'volatile'"), false; } else if (!type.has(type::q_groupshared)) { // Make all global variables 'uniform' by default, since they should be externally visible without the 'static' keyword if (!type.has(type::q_uniform) && !type.is_object()) warning(location, 5000, '\'' + name + "': global variables are considered 'uniform' by default"); // Global variables that are not 'static' are always 'extern' and 'uniform' type.qualifiers |= type::q_extern | type::q_uniform; // It is invalid to make 'uniform' variables constant, since they can be modified externally if (type.has(type::q_const)) return error(location, 3035, '\'' + name + "': variables which are 'uniform' cannot be declared 'const'"), false; } } else { // Static does not really have meaning on local variables if (type.has(type::q_static)) type.qualifiers &= ~type::q_static; if (type.has(type::q_extern)) return error(location, 3006, '\'' + name + "': local variables cannot be declared 'extern'"), false; if (type.has(type::q_uniform)) return error(location, 3047, '\'' + name + "': local variables cannot be declared 'uniform'"), false; if (type.has(type::q_groupshared)) return error(location, 3010, '\'' + name + "': local variables cannot be declared 'groupshared'"), false; if (type.is_object()) return error(location, 3038, '\'' + name + "': local variables cannot be texture, sampler or storage objects"), false; } // The variable name may be followed by an optional array size expression if (!parse_array_size(type)) return false; bool parse_success = true; expression initializer; texture_info texture_info; sampler_info sampler_info; storage_info storage_info; if (accept(':')) { if (!expect(tokenid::identifier)) return false; else if (!global) // Only global variables can have a semantic return error(_token.location, 3043, '\'' + name + "': local variables cannot have semantics"), false; std::string &semantic = texture_info.semantic; semantic = std::move(_token.literal_as_string); // Make semantic upper case to simplify comparison later on std::transform(semantic.begin(), semantic.end(), semantic.begin(), [](std::string::value_type c) { return static_cast(std::toupper(c)); }); } else { // Global variables can have optional annotations if (global && !parse_annotations(sampler_info.annotations)) parse_success = false; // Variables without a semantic may have an optional initializer if (accept('=')) { if (!parse_expression_assignment(initializer)) return false; if (type.has(type::q_groupshared)) return error(initializer.location, 3009, '\'' + name + "': variables declared 'groupshared' cannot have an initializer"), false; // TODO: This could be resolved by initializing these at the beginning of the entry point if (global && !initializer.is_constant) return error(initializer.location, 3011, '\'' + name + "': initial value must be a literal expression"), false; // Check type compatibility if ((type.array_length >= 0 && initializer.type.array_length != type.array_length) || !type::rank(initializer.type, type)) return error(initializer.location, 3017, '\'' + name + "': initial value (" + initializer.type.description() + ") does not match variable type (" + type.description() + ')'), false; if ((initializer.type.rows < type.rows || initializer.type.cols < type.cols) && !initializer.type.is_scalar()) return error(initializer.location, 3017, '\'' + name + "': cannot implicitly convert these vector types (from " + initializer.type.description() + " to " + type.description() + ')'), false; // Deduce array size from the initializer expression if (initializer.type.is_array()) type.array_length = initializer.type.array_length; // Perform implicit cast from initializer expression to variable type if (initializer.type.components() > type.components()) warning(initializer.location, 3206, "implicit truncation of vector type"); initializer.add_cast_operation(type); if (type.has(type::q_static)) initializer.type.qualifiers |= type::q_static; } else if (type.is_numeric() || type.is_struct()) // Numeric variables without an initializer need special handling { if (type.has(type::q_const)) // Constants have to have an initial value return error(location, 3012, '\'' + name + "': missing initial value"), false; else if (!type.has(type::q_uniform)) // Zero initialize all global variables initializer.reset_to_rvalue_constant(location, {}, type); } else if (global && accept('{')) // Textures and samplers can have a property block attached to their declaration { // Non-numeric variables cannot be constants if (type.has(type::q_const)) return error(location, 3035, '\'' + name + "': this variable type cannot be declared 'const'"), false; while (!peek('}')) { if (!expect(tokenid::identifier)) return consume_until('}'), false; const auto property_name = std::move(_token.literal_as_string); const auto property_location = std::move(_token.location); if (!expect('=')) return consume_until('}'), false; backup(); expression expression; if (accept(tokenid::identifier)) // Handle special enumeration names for property values { // Transform identifier to uppercase to do case-insensitive comparison std::transform(_token.literal_as_string.begin(), _token.literal_as_string.end(), _token.literal_as_string.begin(), [](std::string::value_type c) { return static_cast(std::toupper(c)); }); static const std::unordered_map s_enum_values = { { "NONE", 0 }, { "POINT", 0 }, { "LINEAR", 1 }, { "WRAP", uint32_t(texture_address_mode::wrap) }, { "REPEAT", uint32_t(texture_address_mode::wrap) }, { "MIRROR", uint32_t(texture_address_mode::mirror) }, { "CLAMP", uint32_t(texture_address_mode::clamp) }, { "BORDER", uint32_t(texture_address_mode::border) }, { "R8", uint32_t(texture_format::r8) }, { "R16", uint32_t(texture_format::r16) }, { "R16F", uint32_t(texture_format::r16f) }, { "R32I", uint32_t(texture_format::r32i) }, { "R32U", uint32_t(texture_format::r32u) }, { "R32F", uint32_t(texture_format::r32f) }, { "RG8", uint32_t(texture_format::rg8) }, { "R8G8", uint32_t(texture_format::rg8) }, { "RG16", uint32_t(texture_format::rg16) }, { "R16G16", uint32_t(texture_format::rg16) }, { "RG16F", uint32_t(texture_format::rg16f) }, { "R16G16F", uint32_t(texture_format::rg16f) }, { "RG32F", uint32_t(texture_format::rg32f) }, { "R32G32F", uint32_t(texture_format::rg32f) }, { "RGBA8", uint32_t(texture_format::rgba8) }, { "R8G8B8A8", uint32_t(texture_format::rgba8) }, { "RGBA16", uint32_t(texture_format::rgba16) }, { "R16G16B16A16", uint32_t(texture_format::rgba16) }, { "RGBA16F", uint32_t(texture_format::rgba16f) }, { "R16G16B16A16F", uint32_t(texture_format::rgba16f) }, { "RGBA32F", uint32_t(texture_format::rgba32f) }, { "R32G32B32A32F", uint32_t(texture_format::rgba32f) }, { "RGB10A2", uint32_t(texture_format::rgb10a2) }, { "R10G10B10A2", uint32_t(texture_format::rgb10a2) }, }; // Look up identifier in list of possible enumeration names if (const auto it = s_enum_values.find(_token.literal_as_string); it != s_enum_values.end()) expression.reset_to_rvalue_constant(_token.location, it->second); else // No match found, so rewind to parser state before the identifier was consumed and try parsing it as a normal expression restore(); } // Parse right hand side as normal expression if no special enumeration name was matched already if (!expression.is_constant && !parse_expression_multary(expression)) return consume_until('}'), false; if (property_name == "Texture") { // Ignore invalid symbols that were added during error recovery if (expression.base == 0xFFFFFFFF) return consume_until('}'), false; if (!expression.type.is_texture()) return error(expression.location, 3020, "type mismatch, expected texture name"), consume_until('}'), false; if (type.is_sampler() || type.is_storage()) { reshadefx::texture_info &target_info = _codegen->get_texture(expression.base); if (type.is_storage()) // Texture is used as storage target_info.storage_access = true; texture_info = target_info; sampler_info.texture_name = target_info.unique_name; storage_info.texture_name = target_info.unique_name; } } else { if (!expression.is_constant || !expression.type.is_scalar()) return error(expression.location, 3538, "value must be a literal scalar expression"), consume_until('}'), false; // All states below expect the value to be of an integer type expression.add_cast_operation({ type::t_int, 1, 1 }); const int value = expression.constant.as_int[0]; if (value < 0) // There is little use for negative values, so warn in those cases warning(expression.location, 3571, "negative value specified for property '" + property_name + '\''); if (type.is_texture()) { if (property_name == "Width") texture_info.width = value > 0 ? value : 1; else if (type.texture_dimension() >= 2 && property_name == "Height") texture_info.height = value > 0 ? value : 1; else if (type.texture_dimension() >= 3 && property_name == "Depth") texture_info.depth = value > 0 && value <= std::numeric_limits::max() ? static_cast(value) : 1; else if (property_name == "MipLevels") // Also ensures negative values do not cause problems texture_info.levels = value > 0 && value <= std::numeric_limits::max() ? static_cast(value) : 1; else if (property_name == "Format") texture_info.format = static_cast(value); else return error(property_location, 3004, "unrecognized property '" + property_name + '\''), consume_until('}'), false; } else if (type.is_sampler()) { if (property_name == "SRGBTexture" || property_name == "SRGBReadEnable") sampler_info.srgb = value != 0; else if (property_name == "AddressU") sampler_info.address_u = static_cast(value); else if (property_name == "AddressV") sampler_info.address_v = static_cast(value); else if (property_name == "AddressW") sampler_info.address_w = static_cast(value); else if (property_name == "MinFilter") sampler_info.filter = static_cast((uint32_t(sampler_info.filter) & 0x0F) | ((value << 4) & 0x30)); // Combine sampler filter components into a single filter enumeration value else if (property_name == "MagFilter") sampler_info.filter = static_cast((uint32_t(sampler_info.filter) & 0x33) | ((value << 2) & 0x0C)); else if (property_name == "MipFilter") sampler_info.filter = static_cast((uint32_t(sampler_info.filter) & 0x3C) | (value & 0x03)); else if (property_name == "MinLOD" || property_name == "MaxMipLevel") sampler_info.min_lod = static_cast(value); else if (property_name == "MaxLOD") sampler_info.max_lod = static_cast(value); else if (property_name == "MipLODBias" || property_name == "MipMapLodBias") sampler_info.lod_bias = static_cast(value); else return error(property_location, 3004, "unrecognized property '" + property_name + '\''), consume_until('}'), false; } else if (type.is_storage()) { if (property_name == "MipLOD" || property_name == "MipLevel") storage_info.level = value > 0 && value < std::numeric_limits::max() ? static_cast(value) : 0; else return error(property_location, 3004, "unrecognized property '" + property_name + '\''), consume_until('}'), false; } } if (!expect(';')) return consume_until('}'), false; } if (!expect('}')) return false; } } // At this point the array size should be known (either from the declaration or the initializer) if (type.array_length < 0) return error(location, 3074, '\'' + name + "': implicit array missing initial value"), false; symbol symbol; // Variables with a constant initializer and constant type are named constants // Skip this for very large arrays though, to avoid large amounts of duplicated values when that array constant is accessed with a dynamic index if (type.is_numeric() && type.has(type::q_const) && initializer.is_constant && type.array_length < 100) { // Named constants are special symbols symbol = { symbol_type::constant, 0, type, initializer.constant }; } else if (type.is_texture()) { assert(global); texture_info.name = name; texture_info.type = static_cast(type.texture_dimension()); // Add namespace scope to avoid name clashes texture_info.unique_name = 'V' + current_scope().name + name; std::replace(texture_info.unique_name.begin(), texture_info.unique_name.end(), ':', '_'); texture_info.annotations = std::move(sampler_info.annotations); symbol = { symbol_type::variable, 0, type }; symbol.id = _codegen->define_texture(location, texture_info); } // Samplers are actually combined image samplers else if (type.is_sampler()) { assert(global); if (sampler_info.texture_name.empty()) return error(location, 3012, '\'' + name + "': missing 'Texture' property"), false; if (type.texture_dimension() != static_cast(texture_info.type)) return error(location, 3521, '\'' + name + "': type mismatch between texture and sampler type"), false; if (sampler_info.srgb && texture_info.format != texture_format::rgba8) return error(location, 4582, '\'' + name + "': texture does not support sRGB sampling (only textures with RGBA8 format do)"), false; if (texture_info.format == texture_format::r32i ? !type.is_integral() || !type.is_signed() : texture_info.format == texture_format::r32u ? !type.is_integral() || !type.is_unsigned() : !type.is_floating_point()) return error(location, 4582, '\'' + name + "': type mismatch between texture format and sampler element type"), false; sampler_info.name = name; sampler_info.type = type; // Add namespace scope to avoid name clashes sampler_info.unique_name = 'V' + current_scope().name + name; std::replace(sampler_info.unique_name.begin(), sampler_info.unique_name.end(), ':', '_'); symbol = { symbol_type::variable, 0, type }; symbol.id = _codegen->define_sampler(location, texture_info, sampler_info); } else if (type.is_storage()) { assert(global); if (storage_info.texture_name.empty()) return error(location, 3012, '\'' + name + "': missing 'Texture' property"), false; if (type.texture_dimension() != static_cast(texture_info.type)) return error(location, 3521, '\'' + name + "': type mismatch between texture and storage type"), false; if (texture_info.format == texture_format::r32i ? !type.is_integral() || !type.is_signed() : texture_info.format == texture_format::r32u ? !type.is_integral() || !type.is_unsigned() : !type.is_floating_point()) return error(location, 4582, '\'' + name + "': type mismatch between texture format and storage element type"), false; storage_info.name = name; storage_info.type = type; // Add namespace scope to avoid name clashes storage_info.unique_name = 'V' + current_scope().name + name; std::replace(storage_info.unique_name.begin(), storage_info.unique_name.end(), ':', '_'); if (storage_info.level > texture_info.levels - 1) storage_info.level = texture_info.levels - 1; symbol = { symbol_type::variable, 0, type }; symbol.id = _codegen->define_storage(location, texture_info, storage_info); } // Uniform variables are put into a global uniform buffer structure else if (type.has(type::q_uniform)) { assert(global); uniform_info uniform_info; uniform_info.name = name; uniform_info.type = type; uniform_info.annotations = std::move(sampler_info.annotations); uniform_info.initializer_value = std::move(initializer.constant); uniform_info.has_initializer_value = initializer.is_constant; symbol = { symbol_type::variable, 0, type }; symbol.id = _codegen->define_uniform(location, uniform_info); } // All other variables are separate entities else { // Update global variable names to contain the namespace scope to avoid name clashes std::string unique_name = global ? 'V' + current_scope().name + name : name; std::replace(unique_name.begin(), unique_name.end(), ':', '_'); symbol = { symbol_type::variable, 0, type }; symbol.id = _codegen->define_variable(location, type, std::move(unique_name), global, // Shared variables cannot have an initializer type.has(type::q_groupshared) ? 0 : _codegen->emit_load(initializer)); } // Insert the symbol into the symbol table if (!insert_symbol(name, symbol, global)) return error(location, 3003, "redefinition of '" + name + '\''), false; return parse_success; } bool reshadefx::parser::parse_technique() { if (!expect(tokenid::identifier)) return false; technique_info info; info.name = std::move(_token.literal_as_string); bool parse_success = parse_annotations(info.annotations); if (!expect('{')) return false; while (!peek('}')) { if (pass_info pass; parse_technique_pass(pass)) info.passes.push_back(std::move(pass)); else { parse_success = false; if (!peek(tokenid::pass) && !peek('}')) // If there is another pass definition following, try to parse that despite the error return consume_until('}'), false; } } _codegen->define_technique(std::move(info)); return expect('}') && parse_success; } bool reshadefx::parser::parse_technique_pass(pass_info &info) { if (!expect(tokenid::pass)) return false; const auto pass_location = std::move(_token.location); // Passes can have an optional name if (accept(tokenid::identifier)) info.name = std::move(_token.literal_as_string); bool parse_success = true; bool targets_support_srgb = true; function_info vs_info, ps_info, cs_info; if (!expect('{')) return false; while (!peek('}')) { // Parse pass states if (!expect(tokenid::identifier)) return consume_until('}'), false; auto location = std::move(_token.location); const auto state = std::move(_token.literal_as_string); if (!expect('=')) return consume_until('}'), false; const bool is_shader_state = state == "VertexShader" || state == "PixelShader" || state == "ComputeShader"; const bool is_texture_state = state.compare(0, 12, "RenderTarget") == 0 && (state.size() == 12 || (state[12] >= '0' && state[12] < '8')); // Shader and render target assignment looks up values in the symbol table, so handle those separately from the other states if (is_shader_state || is_texture_state) { std::string identifier; scoped_symbol symbol; if (!accept_symbol(identifier, symbol)) return consume_until('}'), false; location = std::move(_token.location); int num_threads[3] = { 1, 1, 1 }; if (accept('<')) { expression x, y, z; if (!parse_expression_multary(x, 8) || !expect(',') || !parse_expression_multary(y, 8)) return consume_until('}'), false; // Parse optional third dimension (defaults to 1) z.reset_to_rvalue_constant({}, 1); if (accept(',') && !parse_expression_multary(z, 8)) return consume_until('}'), false; if (!x.is_constant) return error(x.location, 3011, "value must be a literal expression"), consume_until('}'), false; if (!y.is_constant) return error(y.location, 3011, "value must be a literal expression"), consume_until('}'), false; if (!z.is_constant) return error(z.location, 3011, "value must be a literal expression"), consume_until('}'), false; x.add_cast_operation({ type::t_int, 1, 1 }); y.add_cast_operation({ type::t_int, 1, 1 }); z.add_cast_operation({ type::t_int, 1, 1 }); num_threads[0] = x.constant.as_int[0]; num_threads[1] = y.constant.as_int[0]; num_threads[2] = z.constant.as_int[0]; if (!expect('>')) return consume_until('}'), false; } // Ignore invalid symbols that were added during error recovery if (symbol.id != 0xFFFFFFFF) { if (is_shader_state) { if (!symbol.id) parse_success = false, error(location, 3501, "undeclared identifier '" + identifier + "', expected function name"); else if (!symbol.type.is_function()) parse_success = false, error(location, 3020, "type mismatch, expected function name"); else { // Look up the matching function info for this function definition function_info &function_info = _codegen->get_function(symbol.id); // We potentially need to generate a special entry point function which translates between function parameters and input/output variables switch (state[0]) { case 'V': vs_info = function_info; _codegen->define_entry_point(vs_info, shader_type::vs); info.vs_entry_point = vs_info.unique_name; break; case 'P': ps_info = function_info; _codegen->define_entry_point(ps_info, shader_type::ps); info.ps_entry_point = ps_info.unique_name; break; case 'C': cs_info = function_info; _codegen->define_entry_point(cs_info, shader_type::cs, num_threads); info.cs_entry_point = cs_info.unique_name; break; } } } else { assert(is_texture_state); if (!symbol.id) parse_success = false, error(location, 3004, "undeclared identifier '" + identifier + "', expected texture name"); else if (!symbol.type.is_texture()) parse_success = false, error(location, 3020, "type mismatch, expected texture name"); else if (symbol.type.texture_dimension() != 2) parse_success = false, error(location, 3020, "cannot use texture" + std::to_string(symbol.type.texture_dimension()) + "D as render target"); else { reshadefx::texture_info &target_info = _codegen->get_texture(symbol.id); // Texture is used as a render target target_info.render_target = true; // Verify that all render targets in this pass have the same dimensions if (info.viewport_width != 0 && info.viewport_height != 0 && (target_info.width != info.viewport_width || target_info.height != info.viewport_height)) parse_success = false, error(location, 4545, "cannot use multiple render targets with different texture dimensions (is " + std::to_string(target_info.width) + 'x' + std::to_string(target_info.height) + ", but expected " + std::to_string(info.viewport_width) + 'x' + std::to_string(info.viewport_height) + ')'); info.viewport_width = target_info.width; info.viewport_height = target_info.height; const auto target_index = state.size() > 12 ? (state[12] - '0') : 0; info.render_target_names[target_index] = target_info.unique_name; // Only RGBA8 format supports sRGB writes across all APIs if (target_info.format != texture_format::rgba8) targets_support_srgb = false; } } } else { parse_success = false; } } else // Handle the rest of the pass states { backup(); expression expression; if (accept(tokenid::identifier)) // Handle special enumeration names for pass states { // Transform identifier to uppercase to do case-insensitive comparison std::transform(_token.literal_as_string.begin(), _token.literal_as_string.end(), _token.literal_as_string.begin(), [](std::string::value_type c) { return static_cast(std::toupper(c)); }); static const std::unordered_map s_enum_values = { { "NONE", 0 }, { "ZERO", 0 }, { "ONE", 1 }, { "ADD", uint32_t(pass_blend_op::add) }, { "SUBTRACT", uint32_t(pass_blend_op::subtract) }, { "REVSUBTRACT", uint32_t(pass_blend_op::reverse_subtract) }, { "MIN", uint32_t(pass_blend_op::min) }, { "MAX", uint32_t(pass_blend_op::max) }, { "SRCCOLOR", uint32_t(pass_blend_factor::source_color) }, { "INVSRCCOLOR", uint32_t(pass_blend_factor::one_minus_source_color) }, { "DESTCOLOR", uint32_t(pass_blend_factor::dest_color) }, { "INVDESTCOLOR", uint32_t(pass_blend_factor::one_minus_dest_color) }, { "SRCALPHA", uint32_t(pass_blend_factor::source_alpha) }, { "INVSRCALPHA", uint32_t(pass_blend_factor::one_minus_source_alpha) }, { "DESTALPHA", uint32_t(pass_blend_factor::dest_alpha) }, { "INVDESTALPHA", uint32_t(pass_blend_factor::one_minus_dest_alpha) }, { "KEEP", uint32_t(pass_stencil_op::keep) }, { "REPLACE", uint32_t(pass_stencil_op::replace) }, { "INVERT", uint32_t(pass_stencil_op::invert) }, { "INCR", uint32_t(pass_stencil_op::increment) }, { "INCRSAT", uint32_t(pass_stencil_op::increment_saturate) }, { "DECR", uint32_t(pass_stencil_op::decrement) }, { "DECRSAT", uint32_t(pass_stencil_op::decrement_saturate) }, { "NEVER", uint32_t(pass_stencil_func::never) }, { "EQUAL", uint32_t(pass_stencil_func::equal) }, { "NEQUAL", uint32_t(pass_stencil_func::not_equal) }, { "NOTEQUAL", uint32_t(pass_stencil_func::not_equal) }, { "LESS", uint32_t(pass_stencil_func::less) }, { "GREATER", uint32_t(pass_stencil_func::greater) }, { "LEQUAL", uint32_t(pass_stencil_func::less_equal) }, { "LESSEQUAL", uint32_t(pass_stencil_func::less_equal) }, { "GEQUAL", uint32_t(pass_stencil_func::greater_equal) }, { "GREATEREQUAL", uint32_t(pass_stencil_func::greater_equal) }, { "ALWAYS", uint32_t(pass_stencil_func::always) }, { "POINTS", uint32_t(primitive_topology::point_list) }, { "POINTLIST", uint32_t(primitive_topology::point_list) }, { "LINES", uint32_t(primitive_topology::line_list) }, { "LINELIST", uint32_t(primitive_topology::line_list) }, { "LINESTRIP", uint32_t(primitive_topology::line_strip) }, { "TRIANGLES", uint32_t(primitive_topology::triangle_list) }, { "TRIANGLELIST", uint32_t(primitive_topology::triangle_list) }, { "TRIANGLESTRIP", uint32_t(primitive_topology::triangle_strip) }, }; // Look up identifier in list of possible enumeration names if (const auto it = s_enum_values.find(_token.literal_as_string); it != s_enum_values.end()) expression.reset_to_rvalue_constant(_token.location, it->second); else // No match found, so rewind to parser state before the identifier was consumed and try parsing it as a normal expression restore(); } // Parse right hand side as normal expression if no special enumeration name was matched already if (!expression.is_constant && !parse_expression_multary(expression)) return consume_until('}'), false; else if (!expression.is_constant || !expression.type.is_scalar()) parse_success = false, error(expression.location, 3011, "pass state value must be a literal scalar expression"); // All states below expect the value to be of an unsigned integer type expression.add_cast_operation({ type::t_uint, 1, 1 }); const unsigned int value = expression.constant.as_uint[0]; #define SET_STATE_VALUE_INDEXED(name, info_name, value) \ else if (constexpr size_t name##_len = sizeof(#name) - 1; state.compare(0, name##_len, #name) == 0 && (state.size() == name##_len || (state[name##_len] >= '0' && state[name##_len] < ('0' + static_cast(std::size(info.info_name)))))) \ { \ if (state.size() != name##_len) \ info.info_name[state[name##_len] - '0'] = (value); \ else \ for (int i = 0; i < static_cast(std::size(info.info_name)); ++i) \ info.info_name[i] = (value); \ } if (state == "SRGBWriteEnable") info.srgb_write_enable = (value != 0); SET_STATE_VALUE_INDEXED(BlendEnable, blend_enable, value != 0) else if (state == "StencilEnable") info.stencil_enable = (value != 0); else if (state == "ClearRenderTargets") info.clear_render_targets = (value != 0); SET_STATE_VALUE_INDEXED(ColorWriteMask, color_write_mask, value & 0xFF) SET_STATE_VALUE_INDEXED(RenderTargetWriteMask, color_write_mask, value & 0xFF) else if (state == "StencilReadMask" || state == "StencilMask") info.stencil_read_mask = value & 0xFF; else if (state == "StencilWriteMask") info.stencil_write_mask = value & 0xFF; SET_STATE_VALUE_INDEXED(BlendOp, blend_op, static_cast(value)) SET_STATE_VALUE_INDEXED(BlendOpAlpha, blend_op_alpha, static_cast(value)) SET_STATE_VALUE_INDEXED(SrcBlend, src_blend, static_cast(value)) SET_STATE_VALUE_INDEXED(SrcBlendAlpha, src_blend_alpha, static_cast(value)) SET_STATE_VALUE_INDEXED(DestBlend, dest_blend, static_cast(value)) SET_STATE_VALUE_INDEXED(DestBlendAlpha, dest_blend_alpha, static_cast(value)) else if (state == "StencilFunc") info.stencil_comparison_func = static_cast(value); else if (state == "StencilRef") info.stencil_reference_value = value; else if (state == "StencilPass" || state == "StencilPassOp") info.stencil_op_pass = static_cast(value); else if (state == "StencilFail" || state == "StencilFailOp") info.stencil_op_fail = static_cast(value); else if (state == "StencilZFail" || state == "StencilDepthFail" || state == "StencilDepthFailOp") info.stencil_op_depth_fail = static_cast(value); else if (state == "VertexCount") info.num_vertices = value; else if (state == "PrimitiveType" || state == "PrimitiveTopology") info.topology = static_cast(value); else if (state == "DispatchSizeX") info.viewport_width = value; else if (state == "DispatchSizeY") info.viewport_height = value; else if (state == "DispatchSizeZ") info.viewport_dispatch_z = value; else if (state == "GenerateMipmaps" || state == "GenerateMipMaps") info.generate_mipmaps = (value != 0); else parse_success = false, error(location, 3004, "unrecognized pass state '" + state + '\''); #undef SET_STATE_VALUE_INDEXED } if (!expect(';')) return consume_until('}'), false; } if (parse_success) { if (!info.cs_entry_point.empty()) { if (info.viewport_width == 0 || info.viewport_height == 0) { parse_success = false; error(pass_location, 3012, "pass is missing 'DispatchSizeX' or 'DispatchSizeY' property"); } if (!info.vs_entry_point.empty()) warning(pass_location, 3089, "pass is specifying both 'VertexShader' and 'ComputeShader' which cannot be used together"); if (!info.ps_entry_point.empty()) warning(pass_location, 3089, "pass is specifying both 'PixelShader' and 'ComputeShader' which cannot be used together"); for (codegen::id id : cs_info.referenced_samplers) info.samplers.push_back(_codegen->get_sampler(id)); for (codegen::id id : cs_info.referenced_storages) info.storages.push_back(_codegen->get_storage(id)); } else if (info.vs_entry_point.empty() || info.ps_entry_point.empty()) { parse_success = false; if (info.vs_entry_point.empty()) error(pass_location, 3012, "pass is missing 'VertexShader' property"); if (info.ps_entry_point.empty()) error(pass_location, 3012, "pass is missing 'PixelShader' property"); } else { // Verify that shader signatures between VS and PS match (both semantics and interpolation qualifiers) std::unordered_map vs_semantic_mapping; if (vs_info.return_semantic.empty()) { if (!vs_info.return_type.is_void() && !vs_info.return_type.is_struct()) { parse_success = false; error(pass_location, 3503, '\'' + vs_info.name + "': function return value is missing semantics"); } } else { vs_semantic_mapping[vs_info.return_semantic] = vs_info.return_type; } for (const struct_member_info ¶m : vs_info.parameter_list) { if (param.semantic.empty()) { if (!param.type.is_struct()) { parse_success = false; if (param.type.has(type::q_in)) error(pass_location, 3502, '\'' + vs_info.name + "': input parameter '" + param.name + "' is missing semantics"); else error(pass_location, 3503, '\'' + vs_info.name + "': output parameter '" + param.name + "' is missing semantics"); } } else if (param.type.has(type::q_out)) { vs_semantic_mapping[param.semantic] = param.type; } } if (ps_info.return_semantic.empty()) { if (!ps_info.return_type.is_void() && !ps_info.return_type.is_struct()) { parse_success = false; error(pass_location, 3503, '\'' + ps_info.name + "': function return value is missing semantics"); } } for (const struct_member_info ¶m : ps_info.parameter_list) { if (param.semantic.empty()) { if (!param.type.is_struct()) { parse_success = false; if (param.type.has(type::q_in)) error(pass_location, 3502, '\'' + ps_info.name + "': input parameter '" + param.name + "' is missing semantics"); else error(pass_location, 3503, '\'' + ps_info.name + "': output parameter '" + param.name + "' is missing semantics"); } } else if (param.type.has(type::q_in)) { if (const auto it = vs_semantic_mapping.find(param.semantic); it == vs_semantic_mapping.end() || it->second != param.type) warning(pass_location, 4576, '\'' + ps_info.name + "': input parameter '" + param.name + "' semantic does not match vertex shader one"); else if (((it->second.qualifiers ^ param.type.qualifiers) & (type::q_linear | type::q_noperspective | type::q_centroid | type::q_nointerpolation)) != 0) parse_success = false, error( pass_location, 4568, '\'' + ps_info.name + "': input parameter '" + param.name + "' interpolation qualifiers do not match vertex shader ones"); } } for (codegen::id id : vs_info.referenced_samplers) info.samplers.push_back(_codegen->get_sampler(id)); for (codegen::id id : ps_info.referenced_samplers) info.samplers.push_back(_codegen->get_sampler(id)); if (!vs_info.referenced_storages.empty() || !ps_info.referenced_storages.empty()) { parse_success = false; error(pass_location, 3667, "storage writes are only valid in compute shaders"); } } // Verify render target format supports sRGB writes if enabled if (info.srgb_write_enable && !targets_support_srgb) parse_success = false, error(pass_location, 4582, "one or more render target(s) do not support sRGB writes (only textures with RGBA8 format do)"); } return expect('}') && parse_success; }