2016-10-02 2 views
-1

SDL 2.0.4 및 GLEW 1.13.0을 사용 중이며 SDL2에 일종의 fps 제한이있는 것으로 보입니다. 나는 이것을 알고있다. 왜냐하면 나는 약간의 시간 측정을했기 때문이다.SDL2 FPS 한계를 사용 중지하는 방법

더 구체적으로 말하자면 아무 것도 그리지 않지만 메인 루프에서 SDL_GL_SwapWindow()를 호출하면 각 사이클에 약 16 밀리 초가 소요됩니다. 이 호출 없이는 시간이 거의 걸리지 않습니다. SDL_GL_SwapWindow가 메인 루프에서 유일한 함수 호출 일 때도 16 밀리 초가 소요됩니다. 즉, SDL2에서 fps 제한 또는 vsync를 사용할 수 있어야합니다. 그래서 내 질문은 : 어떻게 제한을 해제합니까?

내 질문과 다소 비슷하게 생긴 유일한 스레드는 다음과 같습니다. SDL_GL_SwapWindow bad performance. 그러나이 실의 답은 실제로 저에게 도움이되지 않습니다. 또한 FreeGLUT을 사용하여 비슷한 코드에서 문제가 발생하지 않기 때문에 이는 내 컴퓨터로 인해 발생하는 것 같지 않습니다. 모든 파일을 통해

간략한 개요 :

jtime.h:  For time measurement 
color.h:  For colored console output (easier to make out errors) 
display.h:  Declaration of class Display 
display.cpp: Implementation of class Display 
main.cpp:  Main function and HandleEvents() 

내 코드 :

MAIN.CPP

#include <iostream> 
#include <conio.h> 

#include <display.h> 
#include <glew.h> 
#include <jtime.h> 

void HandleEvents(SDL_Event&& e, jpk::Display& d) 
{ 
    if(e.type == SDL_QUIT) 
     d.GetWindowState() = jpk::Display::WindowState::EXIT; 
} 

int main(int argc, char* argv[]) 
{ 
    jpk::Display display("Test", SDL_WINDOWPOS_UNDEFINED, 
     SDL_WINDOWPOS_UNDEFINED, 400, 400); 

    if(display.GetWindowState() == jpk::Display::WindowState::EXIT) 
     return 1; 
    else 
    { 
     jpk::measure<> ms; 
     while(display.GetWindowState() != jpk::Display::WindowState::EXIT) 
     { 
      ms.start(); 
      display.Update(50, HandleEvents); 
      std::cout << "Time taken: " << ms.stop() << "ms" << std::endl; 
     } 
     return 0; 
    } 
} 

display.cpp

#include <display.h> 
#include <color.h> 
#include <jtime.h> 

#include <glew.h> 
#include <wglew.h> 
#include <iostream> 
#include <vector> 
#include <list> 

jpk::Display::SDL2Helper jpk::Display::helper(SDL_INIT_MODULES, SDL_OUTPUT_MSG); 

jpk::Display::SDL2Helper::SDL2Helper(const uint32_t& f, const bool& o) : 
    init(true) 
{ 
    jpk::measure<> ms; 
    if(SDL_Init(f)) 
    { 
     std::cerr << jpk::color::light_red_f << "[Error] SDL2 could not be " 
       "initialized.\nDetails according to SDL2: " << SDL_GetError() << 
       jpk::color::reset << std::endl; 
     init = false; 
    } 
    else if(o) 
     std::cout << jpk::color::light_green_f << "[Info] SDL2 has been " << 
       "initialized successfully. Time: " << ms.stop() << "ms." << 
       jpk::color::reset << std::endl; 
} 

jpk::Display::SDL2Helper::~SDL2Helper(void) 
{ 
    if(init) 
     SDL_Quit(); 
} 

jpk::Display::Display(const std::string& t, const unsigned int& x, 
         const unsigned int& y, const unsigned int& w, 
         const unsigned int& h, const uint32_t& f, std::string s) : 
         window(nullptr), 
         glContext(nullptr), 
         state(WindowState::READY) 
{ 
    jpk::measure<> ms; 

    window = SDL_CreateWindow(t.c_str(), x, y, w, h, f | SDL_WINDOW_OPENGL | 
      SDL_WINDOW_HIDDEN); 

    if(window == nullptr) 
    { 
     std::cerr << jpk::color::light_red_f << "[Error] The instance of " << 
       "class 'Display' couldn't be created.\nDetails according to " << 
       "SDL2: " << SDL_GetError() << jpk::color::reset << 
       std::endl; 

     state = WindowState::EXIT; 
    } 
    else 
    { 
     glContext = SDL_GL_CreateContext(window); 

     if(glContext == nullptr) 
     { 
      std::cerr << jpk::color::light_red_f << "[Error] The GL Context " << 
        "of the instance of class 'Display' couldn't be " << 
        "initialized.\nDetails according to SDL2: " << 
        SDL_GetError() << jpk::color::reset << std::endl; 
      state = WindowState::EXIT; 
     } 
     else 
     { 
      GLenum error(glewInit()); 

      if(error != GLEW_OK) 
      { 
       std::cerr << jpk::color::light_red_f << "[Error] GLEW " << 
         "failed to initialize.\nDetails according to GLEW: " << 
         glewGetErrorString(error) << jpk::color::reset << 
         std::endl; 
       state = WindowState::EXIT; 
      } 
      else 
      { 
       bool noSupport(false); 
       if(s.length() > 0) 
       { 
        s += " "; 
        size_t found(s.find(" ")); 

        while(found != std::string::npos) 
        { 
         std::string ext(s); 

         ext.erase(found); 
         s.erase(0, found+1); 

         if(!glewIsSupported(ext.c_str()) && 
           !wglewIsSupported(ext.c_str())) 
         { 
          std::cout << jpk::color::light_red_f << "[Error] " << 
            "The following GLEW extension is not " << 
            "supported: " << ext << "." << 
            jpk::color::reset << std::endl; 
          noSupport = true; 
         } 
         found = s.find(" "); 
        } 
       } 

       if(!noSupport) 
       { 
        std::cout << jpk::color::light_green_f << "[Info] The " << 
          "instance of class 'Display' has successfully " << 
          "been created! Time: " << ms.stop() << "ms." << 
          jpk::color::reset << std::endl; 

        if(!(f & SDL_WINDOW_HIDDEN)) 
         SDL_ShowWindow(window); 
       } 
       else 
        state = WindowState::EXIT; 
      } 
     } 
    } 
} 

jpk::Display::~Display(void) 
{ 
    if(glContext != nullptr) 
     SDL_GL_DeleteContext(glContext); 
    if(window != nullptr) 
     SDL_DestroyWindow(window); 
} 

bool jpk::Display::SDL_InitStatus(void) 
{ 
    return helper.init; 
} 

void jpk::Display::Update(const unsigned int& n, void (*f)(SDL_Event&&, jpk::Display&)) 
{ 
    SDL_GL_SwapWindow(window); 
    static std::list<SDL_Event> events; 

    SDL_Event e; 
    while(SDL_PollEvent(&e)) 
     events.push_back(e); 

    if(n != 0) 
     for(unsigned int i(0); i < n ;i++) 
     { 
      f(std::move(events.front()), *this); 
      events.pop_front(); 
     } 
    else 
    { 
     const unsigned int numEvents(events.size()); 
     for(unsigned int i(0); i < numEvents ;i++) 
     { 
      f(std::move(events.front()), *this); 
      events.pop_front(); 
     } 
    } 
} 

void jpk::Display::Show(void) 
{ 
    if(window != nullptr) 
     SDL_ShowWindow(window); 
} 

void jpk::Display::Hide(void) 
{ 
    if(window != nullptr) 
     SDL_HideWindow(window); 
} 

jpk::Display::WindowState& jpk::Display::GetWindowState(void) 
{ 
    return state; 
} 

display.h

#ifndef DISPLAY_H 
#define DISPLAY_H 

#define SDL_MAIN_HANDLED 

#define SDL_INIT_MODULES SDL_INIT_EVERYTHING 
#define SDL_OUTPUT_MSG false 

#include <string> 
#include <iostream> 
#include <SDL.h> 

namespace jpk 
{ 
    class Display 
    { 
    public: 
     enum class WindowState 
     { 
      READY, 
      EXIT 
     }; 

     Display(const std::string& title, const unsigned int& pos_x, 
       const unsigned int& pos_y, const unsigned int& width, 
       const unsigned int& height, const uint32_t& flags = 0, 
       std::string support = ""); 
     ~Display(void); 

     Display(const Display&) = delete; 
     Display& operator=(const Display&) = delete; 

     static bool SDL_InitStatus(void); 
     WindowState& GetWindowState(void); 

     void Update(const unsigned int& numEvents, 
       void (*eventFunc)(SDL_Event&&, jpk::Display&)); 

     void Show(void); 
     void Hide(void); 

    private: 
     struct SDL2Helper 
     { 
      SDL2Helper(const uint32_t& flags, const bool& output = true); 
      ~SDL2Helper(void); 

      SDL2Helper(const SDL2Helper&) = delete; 
      SDL2Helper& operator=(const SDL2Helper&) = delete; 

      bool init; 
     }; 

     SDL_Window* window; 
     SDL_GLContext glContext; 
     WindowState state; 
     static SDL2Helper helper; 
    }; 
} 

#endif /* DISPLAY_H */ 

jtime.h

#ifndef JTIME_H 
#define JTIME_H 

#include <chrono> 
#include <utility> 

#ifndef CLOCK_TYPE 
#define CLOCK_TYPE std::chrono::steady_clock 
#endif // CLOCK_TYPE 

namespace jpk 
{ 
    template<typename TimeT = std::chrono::milliseconds> class measure 
    { 
    public: 
     measure(void) : 
      t(CLOCK_TYPE::now()) 
     {} 
     ~measure(void) {} 

     measure(const measure&) = delete; 
     measure& operator=(const measure&) = delete; 

     void start(void) 
     { 
      t = CLOCK_TYPE::now(); 
     } 

     typename TimeT::rep stop(void) 
     { 
      return std::chrono::duration_cast<TimeT>(CLOCK_TYPE::now()-t).count(); 
     } 

     TimeT stop_chrono(void) 
     { 
      return std::chrono::duration_cast<TimeT>(CLOCK_TYPE::now()-t); 
     } 

     template<typename F, typename... Args> static 
      typename TimeT::rep duration_single(F func, Args&&... args) 
     { 
      auto start(CLOCK_TYPE::now()); 
      std::forward<decltype(func)>(func)(std::forward<Args>(args)...); 
      return std::chrono::duration_cast<TimeT>(CLOCK_TYPE::now()-start).count(); 
     } 

     template<typename F, typename... Args> static typename TimeT::rep 
      duration_average(const unsigned int& tries, F func, Args&&... args) 
     { 
      typename TimeT::rep* times = new typename TimeT::rep[tries]; 
      typename TimeT::rep time(0.0); 

      for(unsigned int i(0); i < tries ;i++) 
       times[i] = duration_single(func, args...); 

      for(unsigned int i(0); i < tries ;i++) 
       time += times[i]; 

      delete[] times; 
      return double(time)/double(tries); 
     } 

     template<typename F, typename... Args> static 
      TimeT duration_chrono(F func, Args&&... args) 
     { 
      auto start(CLOCK_TYPE::now()); 
      std::forward<decltype(func)>(func)(std::forward<Args>(args)...); 
      return std::chrono::duration_cast<TimeT>(CLOCK_TYPE::now()-start); 
     } 

    private: 
     CLOCK_TYPE::time_point t; 
    }; 

    template<typename TimeT = std::chrono::milliseconds> 
     void wait(const typename TimeT::rep& time) 
    { 
     CLOCK_TYPE::time_point start(CLOCK_TYPE::now()); 
     while(std::chrono::duration_cast<TimeT>(CLOCK_TYPE::now() - start).count() 
       < time) 
     { 
      // Just stop doing anything 
     } 
    } 
} 

#endif /* JTIME_H */ 

color.h

#ifndef COLOR_H 
#define COLOR_H 

#include <string> 
#include <iostream> 

#if defined(_WIN32) && !defined(JPK_USE_ANSI) 
#include <windows.h> 
#endif // _WIN32 

namespace jpk 
{ 
    class color_t 
    { 
    public: 
     color_t(const unsigned int& col); 
#if !defined(_WIN32) || defined(JPK_USE_ANSI) 
     color_t(const std::string& esc); 
#endif // _WIN32 
     virtual ~color_t(void); 

     color_t(const color_t&) = delete; 
     color_t& operator=(const color_t&) = delete; 

     void use(std::ostream& out) const; 
     friend std::ostream& operator<<(std::ostream&, const jpk::color_t&); 

    private: 
#if defined(_WIN32) && !defined(JPK_USE_ANSI) 
     const unsigned int c; 

     static bool reset_attr_got; 
     static WORD reset_attr; 
#else 
     const std::string seq; 
#endif // _WIN32 
    }; 

    struct color 
    { 
     enum colors 
     { 
      BLACK_F, 
      BLUE_F, 
      GREEN_F, 
      CYAN_F, 
      RED_F, 
      MAGENTA_F, 
      BROWN_F, 
      GREY_F, 
      DARKGREY_F, 
      LIGHTBLUE_F, 
      LIGHTGREEN_F, 
      LIGHTCYAN_F, 
      LIGHTRED_F, 
      LIGHTMAGENTA_F, 
      YELLOW_F, 
      WHITE_F, 

      BLACK_B, 
      BLUE_B, 
      GREEN_B, 
      CYAN_B, 
      RED_B, 
      MAGENTA_B, 
      YELLOW_B, 
      WHITE_B, 

      RESET 
     }; 

     color(void) = delete; 
     ~color(void) = delete; 

     static color_t black_f; 
     static color_t red_f; 
     static color_t green_f; 
     static color_t brown_f; 
     static color_t blue_f; 
     static color_t magenta_f; 
     static color_t cyan_f; 
     static color_t grey_f; 
     static color_t dark_grey_f; 
     static color_t light_red_f; 
     static color_t light_green_f; 
     static color_t yellow_f; 
     static color_t light_blue_f; 
     static color_t light_magenta_f; 
     static color_t light_cyan_f; 
     static color_t white_f; 

     static color_t black_b; 
     static color_t red_b; 
     static color_t green_b; 
     static color_t yellow_b; 
     static color_t blue_b; 
     static color_t magenta_b; 
     static color_t cyan_b; 
     static color_t white_b; 

     static color_t reset; 
    }; 
} 

#if !defined(_WIN32) || defined(JPK_USE_ANSI) 
    std::string getAnsiEsc(const unsigned int& col) 
    { 
     switch(col) 
     { 
     case jpk::color::BLACK_F:   return "\033[22;30m"; 
     case jpk::color::RED_F:    return "\033[22;31m"; 
     case jpk::color::GREEN_F:   return "\033[22;32m"; 
     case jpk::color::BROWN_F:   return "\033[22;33m"; 
     case jpk::color::BLUE_F:   return "\033[22;34m"; 
     case jpk::color::MAGENTA_F:   return "\033[22;35m"; 
     case jpk::color::CYAN_F:   return "\033[22;36m"; 
     case jpk::color::GREY_F:   return "\033[22;37m"; 
     case jpk::color::DARKGREY_F:  return "\033[01;30m"; 
     case jpk::color::LIGHTRED_F:  return "\033[01;31m"; 
     case jpk::color::LIGHTGREEN_F:  return "\033[01;32m"; 
     case jpk::color::YELLOW_F:   return "\033[01;33m"; 
     case jpk::color::LIGHTBLUE_F:  return "\033[01;34m"; 
     case jpk::color::LIGHTMAGENTA_F: return "\033[01;35m"; 
     case jpk::color::LIGHTCYAN_F:  return "\033[01;36m"; 
     case jpk::color::WHITE_F:   return "\033[01;37m"; 

     case jpk::color::BLACK_B:   return "\033[40m"; 
     case jpk::color::RED_B:    return "\033[41m"; 
     case jpk::color::GREEN_B:   return "\033[42m"; 
     case jpk::color::YELLOW_B:   return "\033[43m"; 
     case jpk::color::BLUE_B:   return "\033[44m"; 
     case jpk::color::MAGENTA_B:   return "\033[45m"; 
     case jpk::color::CYAN_B:   return "\033[46m"; 
     case jpk::color::WHITE_B:   return "\033[47m"; 

     case jpk::color::RESET:    return "\033[0m"; 
     } 
     return ""; 
    } 
#endif // _WIN32 

jpk::color_t::color_t(const unsigned int& col) : 
#if defined(_WIN32) && !defined(JPK_USE_ANSI) 
     c(col) 
    { 
     if(!reset_attr_got) 
     { 
      CONSOLE_SCREEN_BUFFER_INFO csbi; 
      GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi); 

      reset_attr = csbi.wAttributes; 
      reset_attr_got = true; 
     } 
#else 
     seq(jpk::getAnsiEsc(col)) 
    {} 

    jpk::color_t::color_t(const std::string& esc) : 
     seq(esc) 
    { 
#endif // _WIN32 
} 

jpk::color_t::~color_t(void) {} 

#if defined(_WIN32) && !defined(JPK_USE_ANSI) 
    bool jpk::color_t::reset_attr_got(false); 
    WORD jpk::color_t::reset_attr(0); 

    void jpk::color_t::use(std::ostream& out) const 
    { 
     if(c <= jpk::color::RESET) 
     { 
      HANDLE hConsole(GetStdHandle(STD_OUTPUT_HANDLE)); 
      CONSOLE_SCREEN_BUFFER_INFO csbi; 
      GetConsoleScreenBufferInfo(hConsole, &csbi); 

      if(c < jpk::color::BLACK_B) 
       SetConsoleTextAttribute(hConsole, (csbi.wAttributes & 0xFFF0) | (WORD)c); 
      else if((c > jpk::color::WHITE_F) && (c < jpk::color::RESET)) 
       SetConsoleTextAttribute(hConsole, (csbi.wAttributes & 0xFF0F) | (((WORD)(c - jpk::color::BLACK_B)) << 4)); 
      else if(c == jpk::color::RESET) 
       SetConsoleTextAttribute(hConsole, reset_attr); 
     } 
    } 

    jpk::color_t jpk::color::black_f(jpk::color::BLACK_F); 
    jpk::color_t jpk::color::red_f(jpk::color::RED_F); 
    jpk::color_t jpk::color::green_f(jpk::color::GREEN_F); 
    jpk::color_t jpk::color::brown_f(jpk::color::BROWN_F); 
    jpk::color_t jpk::color::blue_f(jpk::color::BLUE_F); 
    jpk::color_t jpk::color::magenta_f(jpk::color::MAGENTA_F); 
    jpk::color_t jpk::color::cyan_f(jpk::color::CYAN_F); 
    jpk::color_t jpk::color::grey_f(jpk::color::GREY_F); 
    jpk::color_t jpk::color::dark_grey_f(jpk::color::DARKGREY_F); 
    jpk::color_t jpk::color::light_red_f(jpk::color::LIGHTRED_F); 
    jpk::color_t jpk::color::light_green_f(jpk::color::LIGHTGREEN_F); 
    jpk::color_t jpk::color::yellow_f(jpk::color::YELLOW_F); 
    jpk::color_t jpk::color::light_blue_f(jpk::color::LIGHTBLUE_F); 
    jpk::color_t jpk::color::light_magenta_f(jpk::color::LIGHTMAGENTA_F); 
    jpk::color_t jpk::color::light_cyan_f(jpk::color::LIGHTCYAN_F); 
    jpk::color_t jpk::color::white_f(jpk::color::WHITE_F); 

    jpk::color_t jpk::color::black_b(jpk::color::BLACK_B); 
    jpk::color_t jpk::color::red_b(jpk::color::RED_B); 
    jpk::color_t jpk::color::green_b(jpk::color::GREEN_B); 
    jpk::color_t jpk::color::yellow_b(jpk::color::YELLOW_B); 
    jpk::color_t jpk::color::blue_b(jpk::color::BLUE_B); 
    jpk::color_t jpk::color::magenta_b(jpk::color::MAGENTA_B); 
    jpk::color_t jpk::color::cyan_b(jpk::color::CYAN_B); 
    jpk::color_t jpk::color::white_b(jpk::color::WHITE_B); 

    jpk::color_t jpk::color::reset(jpk::color::RESET); 
#else 
    void jpk::color_t::use(std::ostream& out) const 
    { 
     out << seq; 
    } 

    jpk::color_t jpk::color::black_f("\033[22;30m"); 
    jpk::color_t jpk::color::red_f("\033[22;31m"); 
    jpk::color_t jpk::color::green_f("\033[22;32m"); 
    jpk::color_t jpk::color::brown_f("\033[22;33m"); 
    jpk::color_t jpk::color::blue_f("\033[22;34m"); 
    jpk::color_t jpk::color::magenta_f("\033[22;35m"); 
    jpk::color_t jpk::color::cyan_f("\033[22;36m"); 
    jpk::color_t jpk::color::grey_f("\033[22;37m"); 
    jpk::color_t jpk::color::dark_grey_f("\033[01;30m"); 
    jpk::color_t jpk::color::light_red_f("\033[01;31m"); 
    jpk::color_t jpk::color::light_green_f("\033[01;32m"); 
    jpk::color_t jpk::color::yellow_f("\033[01;33m"); 
    jpk::color_t jpk::color::light_blue_f("\033[01;34m"); 
    jpk::color_t jpk::color::light_magenta_f("\033[01;35m"); 
    jpk::color_t jpk::color::light_cyan_f("\033[01;36m"); 
    jpk::color_t jpk::color::white_f("\033[01;37m"); 

    jpk::color_t jpk::color::black_b("\033[40m"); 
    jpk::color_t jpk::color::red_b("\033[41m"); 
    jpk::color_t jpk::color::green_b("\033[42m"); 
    jpk::color_t jpk::color::yellow_b("\033[43m"); 
    jpk::color_t jpk::color::blue_b("\033[44m"); 
    jpk::color_t jpk::color::magenta_b("\033[45m"); 
    jpk::color_t jpk::color::cyan_b("\033[46m"); 
    jpk::color_t jpk::color::white_b("\033[47m"); 

    jpk::color_t jpk::color::reset("\033[0m"); 
#endif // _WIN32 

namespace jpk 
{ 
    std::ostream& operator<<(std::ostream& out, const color_t& col) 
    { 
     col.use(out); 
     return out; 
    } 
} 

#endif /* COLOR_H */ 

답변

1

이 SDL로하지만 그래픽 드라이버의 OpenGL 설정과는 아무 상관이 없습니다. 버퍼를 스와핑 할 때 작업이 디스플레이 새로 고침과 동기화되어 찢어지지 않는 아티팩트가 나타나지 않을 수 있습니다. 프로그램이 표시되는 것보다 빠르게 표시되는 경우 표시되는 단일 프레임 내에서 여러 "줄무늬"의 렌더링이 나타나며 각 렌더링이 첫 번째 렌더링 시간만큼 지연됩니다. 따라서 일반적으로 실제로 다시 그리기를 FPS로 제한하고 화면 새로 고침에 동기화해야합니다. 프로그램 내에서 선택적으로 활성화 또는 비활성화 할 수있는 여러 가지 API가 있습니다 (드라이버가 항상이를 무시할 수는 있지만). OpenGL (API 인 SDL_GL_SwapBuffers)의 경우이를 제어하기 위해 API를 거치며이를 "스왑 간격"API라고합니다. 자세한 내용은 OpenGL wiki에서 확인할 수 있습니다. https://www.opengl.org/wiki/Swap_Interval

+0

그렇다면 SDL2를 사용할 때 이런 현상이 발생하지만 FreeGLUT을 사용하면 정확히 동일한 작업을 수행 할 수없는 이유는 무엇입니까? FreeGLUT이 사용 중지합니까? – JPKing64

+0

@ JPKing64 : SDL2는 실제로 스왑 간격 API (https://wiki.libsdl.org/SDL_GL_SetSwapInterval 참조)에 대해 "아는"것으로, 단순히 특정 초기화 값으로 설정하는 반면, FreeGLUT에는 스왑 간격 API에 대한 래퍼가 없습니다 컨텍스트를 생성 한 후 "있는 그대로"남겨 둡니다. – datenwolf

+0

도움을 주셔서 감사합니다. 추가 조사가 끝나면 어떻게 작동하는지 알았습니다. 여전히 시간이 있다면 SDL2는 새로운 이미지를 표시 할 때까지 아무것도하지 않습니다. 매주기마다 3ms를 기다리더라도 fps는 대부분 60으로 유지됩니다. 솔직히 말해서 SDL2가 지금 당장은 그렇게 할 것입니다. 그러나 나는 미래의 어느 시점에서 fps 제한을 사용하지 않기 위해 편리하게 사용할 수있을 것이라고 생각 했으므로 Display 클래스에서 스왑 간격을 비활성화하는 옵션을 여전히 구현할 것입니다. – JPKing64

관련 문제